<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:blogger='http://schemas.google.com/blogger/2008' xmlns:georss='http://www.georss.org/georss' xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-29815492</id><updated>2026-08-18T00:08:03.492-07:00</updated><category term="predictive modeling"/><category term="Data Mining"/><category term="Data Manipulation"/><category term="SVD"/><category term="PROC PRINCOMP"/><category term="Macro Programming"/><category term="PROC SCORE"/><category term="PROC DISCRIM"/><category term="PROC REG"/><category term="Array"/><category term="Boost Algorithms"/><category term="Hash Object"/><category term="keras"/><category term="K-means Clustering"/><category term="KNN"/><category term="Moore-Penrose pseudoinverse"/><category term="Nearest Neighbor"/><category term="PCA"/><category term="PROC CORR"/><category term="PROC FASTCLUS"/><category term="PROC GLIMMIX"/><category term="PROC HPMIXED"/><category term="PROC MEANS"/><category term="PROC SQL"/><category term="Python"/><category term="R"/><category term="deep learning"/><category term="Bayesian"/><category term="CNN"/><category term="GRAPH"/><category term="HOSVD"/><category term="Index"/><category term="PROC DISTANCE"/><category term="PROC EXPAND"/><category term="PROC GLMSELECT"/><category term="PROC GPLOT"/><category term="PROC PLS"/><category term="PROC UNIVARIATE"/><category term="SAS"/><category term="Tensor"/><category term="kernel"/><category term="AI"/><category term="AUC"/><category term="Erlang C"/><category term="Filter"/><category term="Finite Mixture Model"/><category term="Format"/><category term="Gap Statistic"/><category term="Gini Index"/><category term="HPGLIMMIX"/><category term="Heckman Selection model"/><category term="Jupyter Notebook"/><category term="K/N Algorithm"/><category term="LGD"/><category term="Over-dispersion"/><category term="PROC APPEND"/><category term="PROC CANDISC"/><category term="PROC FACTOR"/><category term="PROC FCMP"/><category term="PROC FMM"/><category term="PROC FORMAT"/><category term="PROC GENDMO"/><category term="PROC GLMMOD"/><category term="PROC KRIGE2D"/><category term="PROC LIFEREG"/><category term="PROC MIXED"/><category term="PROC MODECLUS"/><category term="PROC NPAR1WAY"/><category term="PROC ORTHOREG"/><category term="PROC QLIM"/><category term="PROC STANDARD"/><category term="PROC STDIZE"/><category term="Queueing Model"/><category term="RISK"/><category term="Random Split"/><category term="Statistical Graphics"/><category term="Theano"/><category term="Tobit Model"/><category term="multi-threading"/><category term="quantile computing"/><category term="random number"/><title type='text'>SAS Programming for Data Mining</title><subtitle type='html'>© Copyright 2006-2015 /&#xa;SAS® and all other SAS Institute Inc. product or service names are registered trademarks or trademarks of SAS Institute Inc. in the USA and other countries. ® indicates USA registration. Other brand and product names are trademarks of their respective companies.</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default?redirect=false'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><link rel='next' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default?start-index=26&amp;max-results=25&amp;redirect=false'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>78</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-29815492.post-882657932533354709</id><published>2018-11-23T10:39:00.000-08:00</published><updated>2018-11-24T11:25:11.716-08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="CNN"/><category scheme="http://www.blogger.com/atom/ns#" term="keras"/><category scheme="http://www.blogger.com/atom/ns#" term="kernel"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><title type='text'>Visualize your own image filter</title><content type='html'>First load necessary packages
&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border-bottom: #999999 1px dashed; border-left: #999999 1px dashed; border-right: #999999 1px dashed; border-top: #999999 1px dashed; color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; width: 100%;&quot;&gt;&lt;code&gt;
import pandas as pd
import numpy as np
from keras.models import Sequential
from keras.layers import Dense, Dropout, Conv2D
import keras.backend as K
import scipy, imageio
import matplotlib.pyplot as plt
from PIL import Image
%matplotlib inline
&lt;/code&gt;&lt;/pre&gt;
Then show original picture of my Jeep

&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border-bottom: #999999 1px dashed; border-left: #999999 1px dashed; border-right: #999999 1px dashed; border-top: #999999 1px dashed; color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; width: 100%;&quot;&gt;&lt;code&gt;
# 首先将图片读入为矩阵
# 我们可以用pyplot的imshow()方法来展示图片
# 这是我曾经拥有的牧马人JK Rubicon Unlimited
#
img_data = imageio.imread(&#39;./pics/wranglerJK.jpg&#39;)
print(img_data.data.shape)

img = Image.fromarray(img_data, &#39;RGB&#39;)
plt.imshow(img)

&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgH8lFqULfQ3pUiX56ORlRk6vFJRFVrDO5iaxwFOkcRqWyVUAL6yO10H8k9DIkYQwNq1yfCXdyHSKRo-Va3EOK27njkM60Mr2ko6EHK4CyQjE-LYKu6mhjBOSsUTzzIhrJ3Nv6s-g/s1600/wranglerJK.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1200&quot; data-original-width=&quot;1600&quot; height=&quot;480&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgH8lFqULfQ3pUiX56ORlRk6vFJRFVrDO5iaxwFOkcRqWyVUAL6yO10H8k9DIkYQwNq1yfCXdyHSKRo-Va3EOK27njkM60Mr2ko6EHK4CyQjE-LYKu6mhjBOSsUTzzIhrJ3Nv6s-g/s640/wranglerJK.jpg&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi467i50RvXn-_RRY3XVhrDMeIzskiYkcxiitImvB9Bo4bmBg4thucA7d_NUX61bzDK1kLv6kyLnMnGMVEAn7f4GeC_svTihJ-zO7E4qEDRDENYJjnIeZX9vvrrLy7J7H5Fff49Fg/s1600/wranglerJK2.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;639&quot; data-original-width=&quot;960&quot; height=&quot;426&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi467i50RvXn-_RRY3XVhrDMeIzskiYkcxiitImvB9Bo4bmBg4thucA7d_NUX61bzDK1kLv6kyLnMnGMVEAn7f4GeC_svTihJ-zO7E4qEDRDENYJjnIeZX9vvrrLy7J7H5Fff49Fg/s640/wranglerJK2.jpg&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;添加图片说明&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;br /&gt;
Now, build our 2-D convolutional function that takes a custom filter matrix and comput the filtered output image matrix.
&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border-bottom: #999999 1px dashed; border-left: #999999 1px dashed; border-right: #999999 1px dashed; border-top: #999999 1px dashed; color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; width: 100%;&quot;&gt;&lt;code&gt;def my_init(shape, dtype=None):
    new_mat = np.zeros((shape[0], shape[1], 3, 3))
    for i in range(shape[0]):
        for j in range(shape[1]):
            new_mat[:, :, i, j] = filter_mat
    return np.array(new_mat, dtype=dtype)


def MyFilter(filter_mat):
    print(len(filter_mat.shape))
    if len(filter_mat.shape)!=2:
        print(&#39;Invalid filter matrix. It must be 2-D&#39;)
        return []
    else:
        kernel_size=filter_mat.shape
        row, col, depth = img_data.shape
        input_shape=img_data.shape
        filter_size = row*col*depth
        print(filter_size)


        model = Sequential()
        model.add(Conv2D(depth, 
                         kernel_size=kernel_size, 
                         input_shape=input_shape, 
                         padding=&#39;same&#39;, 
                         activation=&#39;linear&#39;, 
                         data_format=&#39;channels_last&#39;,
                         kernel_initializer=my_init,
                         name=&#39;Conv&#39;)                         
                  )
        model.add(Dense(1, activation=&#39;linear&#39;))
        model.compile(optimizer=&#39;sgd&#39;, loss=&#39;mse&#39;)
        model.summary()


        inX = model.input                                          
        outputs = [layer.output for layer in model.layers if layer.name==&#39;Conv&#39;]     
        functions = [K.function([inX], [out]) for out in outputs]   

        layer_outs = [func([img_data.reshape(1, row, col, depth)]) for func in functions]
        activationLayer = layer_outs[0][0]
        temp = (activationLayer-np.min(activationLayer))
        normalized_activationLayer = temp/np.max( np.max(temp))
        return(normalized_activationLayer.reshape(row, col, depth))
&lt;/code&gt;&lt;/pre&gt;
Now, insert our own fixed filter matrix and get the output, using pyplot.imshow() to display the filtered picture. This time we throw in an edge detector.&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border-bottom: #999999 1px dashed; border-left: #999999 1px dashed; border-right: #999999 1px dashed; border-top: #999999 1px dashed; color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; width: 100%;&quot;&gt;&lt;code&gt;filter_mat = np.array([-1, -2, -3, 0, 0, 0, 1, 2, 3]).reshape(3, 3)

outLayer = MyFilter(filter_mat)
plt.imshow(outLayer)
&lt;/code&gt;&lt;/pre&gt;
Below is the filtered picture.&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;/div&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2Ih9E1vXiYLTI0VujSST-puH0jSSibeaUtl9C4T_jKYRjyWtZUp7LlwtpOTxEct4SH8oC3LBErfmkJb6nZruLjmmPDgQGjysUUjLa2uqsTx10LaGjLDZ_QeXNVRPVlvuU5PuJYw/s1600/filtered.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;

&lt;img border=&quot;0&quot; data-original-height=&quot;578&quot; data-original-width=&quot;774&quot; height=&quot;476&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2Ih9E1vXiYLTI0VujSST-puH0jSSibeaUtl9C4T_jKYRjyWtZUp7LlwtpOTxEct4SH8oC3LBErfmkJb6nZruLjmmPDgQGjysUUjLa2uqsTx10LaGjLDZ_QeXNVRPVlvuU5PuJYw/s640/filtered.png&quot; width=&quot;640&quot; /&gt;&amp;nbsp;&lt;/a&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3xmip6m0mYj9HIHHuThuO_GWEHcxQSm1XKXTp_ZkjhVHY8jAMVWAM8oqH60sxccjnr-PPf_91GImmCao-AajGgxRsXu104cLBg_e5i7TnJy8ZkKqLJQI_9W9opr_1nUgz-IlhRA/s1600/filtered2.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;578&quot; data-original-width=&quot;860&quot; height=&quot;428&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3xmip6m0mYj9HIHHuThuO_GWEHcxQSm1XKXTp_ZkjhVHY8jAMVWAM8oqH60sxccjnr-PPf_91GImmCao-AajGgxRsXu104cLBg_e5i7TnJy8ZkKqLJQI_9W9opr_1nUgz-IlhRA/s640/filtered2.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/882657932533354709/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/882657932533354709' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/882657932533354709'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/882657932533354709'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2018/11/visualize-your-own-image-fitler.html' title='Visualize your own image filter'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgH8lFqULfQ3pUiX56ORlRk6vFJRFVrDO5iaxwFOkcRqWyVUAL6yO10H8k9DIkYQwNq1yfCXdyHSKRo-Va3EOK27njkM60Mr2ko6EHK4CyQjE-LYKu6mhjBOSsUTzzIhrJ3Nv6s-g/s72-c/wranglerJK.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-8207956113478445977</id><published>2017-12-22T00:44:00.002-08:00</published><updated>2018-11-23T01:40:56.825-08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="deep learning"/><category scheme="http://www.blogger.com/atom/ns#" term="keras"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><title type='text'>Visualize activations functions using keras</title><content type='html'>In &lt;a href=&quot;http://keras.io/&quot; target=&quot;_blank&quot;&gt;keras&lt;/a&gt;, we can visualize &lt;a href=&quot;https://en.wikipedia.org/wiki/Activation_function&quot; target=&quot;_blank&quot;&gt;activation functions&lt;/a&gt;&#39; geometric properties using backend functions over layers of a model.
&lt;br /&gt;
&lt;br /&gt;
We all know the exact function of popular activation functions such as &#39;&lt;i&gt;&lt;b&gt;sigmoid&lt;/b&gt;&lt;/i&gt;&#39;, &#39;&lt;b&gt;&lt;i&gt;tanh&lt;/i&gt;&lt;/b&gt;&#39;, &#39;&lt;i&gt;&lt;b&gt;relu&lt;/b&gt;&lt;/i&gt;&#39;, etc, and we can feed data to these functions to directly obtain their output. But how to do that via keras without explicitly specifying their functional forms? &lt;br /&gt;
&lt;br /&gt;
This can be done following the four steps below:
&lt;br /&gt;
&lt;br /&gt;
1. define a simple MLP model with a one dimension input data, a one neuron dense network as the hidden layer, and the output layer will have a &#39;&lt;b&gt;&lt;i&gt;linear&lt;/i&gt;&lt;/b&gt;&#39; activation function for one neuron.
&lt;br /&gt;
2. Extract layers&#39; output of the model (fitted or not) via iterating through &lt;b&gt;model.layers
&lt;/b&gt;&lt;br /&gt;
3. Using backend function &lt;b&gt;K.function()&lt;/b&gt; to obtain calculated output for a given input data
&lt;br /&gt;
4. Feed desired data to the above functions to obtain the output from appropriate activation function.
&lt;br /&gt;
&lt;br /&gt;
The code below is a demo:
&lt;br /&gt;
&lt;code&gt;
&lt;/code&gt;
&lt;br /&gt;
&lt;pre style=&quot;border-bottom: #999999 1px dashed; border-left: #999999 1px dashed; border-right: #999999 1px dashed; border-top: #999999 1px dashed; color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; width: 100%;&quot;&gt;&lt;div&gt;
&lt;code&gt;

from keras.layers import Dense, Activation
from keras.models import Sequential
import keras.backend as K
import numpy as np
import matplotlib.pyplot as plt&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;
# 以下设置显示中文文方法根据 http://blog.csdn.net/rumswell/article/details/6544377 
plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] #指定默认字体  
plt.rcParams[&#39;axes.unicode_minus&#39;] = False #解决图像中中文符号显示为方块的问题  

def NNmodel(activationFunc=&#39;linear&#39;):
    &#39;&#39;&#39;
       定义一个神经网络模型。如果要定义不同的模型，可以直接修改该函数
    &#39;&#39;&#39;
    if (activationFunc==&#39;softplus&#39;) | (activationFunc==&#39;sigmoid&#39;):
        winit=&#39;lecun_uniform&#39;
    elif activationFunc==&#39;hard_sigmoid&#39;:
        winit=&#39;lecun_normal&#39;
    else:
        winit=&#39;he_uniform&#39;
    model = Sequential()
    model.add(Dense(1, input_shape=(1,), activation=activationFunc, 
                    kernel_initializer=winit, 
                    name=&#39;Hidden&#39;))
        
    model.add(Dense(1, activation=&#39;linear&#39;, name=&#39;Output&#39;))   
    model.compile(loss=&#39;mse&#39;, optimizer=&#39;sgd&#39;)    
    return model

def VisualActivation(activationFunc=&#39;relu&#39;, plot=True):    
    x = (np.arange(100)-50)/10
    y = np.log(x+x.max()+1)
    
    model = NNmodel(activationFunc = activationFunc)
    
    inX = model.input                                          
    outputs = [layer.output for layer in model.layers if layer.name==&#39;Hidden&#39;]     
    functions = [K.function([inX], [out]) for out in outputs]   
    
    layer_outs = [func([x.reshape(-1, 1)]) for func in functions]
    activationLayer = layer_outs[0][0]
        
    activationDf = pd.DataFrame(activationLayer)
    result=pd.concat([pd.DataFrame(x), activationDf], axis=1)
    result.columns=[&#39;X&#39;, &#39;Activated&#39;]
    result.set_index(&#39;X&#39;, inplace=True)
    if plot:
        result.plot(title=f)
    
    return result


# Now we can visualize them (assuming default settings) :
actFuncs = [&#39;linear&#39;, &#39;softmax&#39;, &#39;sigmoid&#39;, &#39;tanh&#39;, &#39;softsign&#39;, &#39;hard_sigmoid&#39;, &#39;softplus&#39;, &#39;selu&#39;, &#39;elu&#39;]

from keras.layers import LeakyReLU
figure = plt.figure()
for i, f in enumerate(actFuncs):
    # 依次画图
    figure.add_subplot(3, 3, i+1)
    out=VisualActivation(activationFunc=f, plot=False)
    plt.plot(out.index, out.Activated)
    plt.title(u&#39;激活函数：&#39;+f)&lt;/code&gt;&lt;/div&gt;
&lt;/pre&gt;
This figure is the output from above code. As we can see, the geometric property of each activation function is well captured.

&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgMSzwX95oLa1BCnd602q30CU2bvkHaz9V93Zx2ROA7XVUCJBnTAp9G6H2RvhHGOEUqHf5nniDgHBHB8ezzdzHcZGeVJMZxC05WXbumo7aih1s74cGF1X0GJMGBI4_sSRohbQBcjw/s1600/activationFig1.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;589&quot; data-original-width=&quot;937&quot; height=&quot;402&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgMSzwX95oLa1BCnd602q30CU2bvkHaz9V93Zx2ROA7XVUCJBnTAp9G6H2RvhHGOEUqHf5nniDgHBHB8ezzdzHcZGeVJMZxC05WXbumo7aih1s74cGF1X0GJMGBI4_sSRohbQBcjw/s640/activationFig1.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;/div&gt;
</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/8207956113478445977'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/8207956113478445977'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2017/12/visualize-activations-functions-using.html' title='Visualize activations functions using keras'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgMSzwX95oLa1BCnd602q30CU2bvkHaz9V93Zx2ROA7XVUCJBnTAp9G6H2RvhHGOEUqHf5nniDgHBHB8ezzdzHcZGeVJMZxC05WXbumo7aih1s74cGF1X0GJMGBI4_sSRohbQBcjw/s72-c/activationFig1.png" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-3998091181104122036</id><published>2017-09-06T17:48:00.002-07:00</published><updated>2017-09-15T00:52:34.672-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="AI"/><category scheme="http://www.blogger.com/atom/ns#" term="CNN"/><category scheme="http://www.blogger.com/atom/ns#" term="deep learning"/><category scheme="http://www.blogger.com/atom/ns#" term="keras"/><title type='text'>A VGG-like CNN in keras for Fashion-MNIST with 94% accuracy</title><content type='html'>In many introductory to image recognition tasks, the famous &lt;a href=&quot;http://yann.lecun.com/exdb/mnist/&quot;&gt;MNIST&lt;/a&gt; data set is typically used. However, there are some issues with this data:&lt;br /&gt;
&lt;br /&gt;
1. It is too easy. For example, a simple MLP model can achieve 99% accuracy, and a 2-layer CNN can achieve 99% accuracy.&lt;br /&gt;
&lt;br /&gt;
2. It is over used. Literally every machine learning introductory article or image recognition task will use this data set as benchmark. But because it is so easy to get nearly perfect classification result, its usefulness is discounted and is not really useful for modern machine learning/AI tasks.&lt;br /&gt;
&lt;br /&gt;
Therefore, there appears &lt;a href=&quot;https://github.com/zalandoresearch/fashion-mnist&quot;&gt;Fashion-MNIST&lt;/a&gt; dataset. This dataset is developed as a direct replacement for MNIST data in the sense that:&lt;br /&gt;
&lt;br /&gt;
1. It is the same size and style: 28x28 grayscale image&lt;br /&gt;
2. Each image is associated with 1 out of 10 classes, which are:&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 0:T-shirt/top,&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 1:Trouser,&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 2:Pullover,&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 3:Dress,&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 4:Coat,&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 5:Sandal,&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 6:Shirt,&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 7:Sneaker,&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 8:Bag,&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 9:Ankle boot&lt;br /&gt;
3. 60000 training sample and 10000 testing sample

Here is a snapshot of some samples:
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiq-W6_UdsMgEOQV0et7tbbLI9D3bgrxmRvDBG4Orw4WaddfMkezPwpl38U5Q__QqLV84mQQngMS29CimMGV7mvRKnfIWFCdVkuZ-85Sr8QQNoSVdH2bVXhW5sUxhiIwl0btAGzjg/s1600/fashion-mnist-sprite.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;840&quot; data-original-width=&quot;840&quot; height=&quot;640&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiq-W6_UdsMgEOQV0et7tbbLI9D3bgrxmRvDBG4Orw4WaddfMkezPwpl38U5Q__QqLV84mQQngMS29CimMGV7mvRKnfIWFCdVkuZ-85Sr8QQNoSVdH2bVXhW5sUxhiIwl0btAGzjg/s640/fashion-mnist-sprite.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
Since its appearance, there have been multiple submissions to benchmark this data, and some of them are able to achieve 95%+ accuracy, most noticeably Residual network or separable CNN.
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiH2lWY1-dkWxUSFStTnqG7oUziDRMSCtIFfTLaZ-T5UQ2ZmiX5ko2Gy1V474GIS9Cur0yom-S7_y1gA4zAyv6ey9HgqLEm-HV2Y1C4F7Nrd97vQjg7RKtxIR1hccahuduwnnPutA/s1600/benchmark.PNG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;916&quot; data-original-width=&quot;931&quot; height=&quot;630&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiH2lWY1-dkWxUSFStTnqG7oUziDRMSCtIFfTLaZ-T5UQ2ZmiX5ko2Gy1V474GIS9Cur0yom-S7_y1gA4zAyv6ey9HgqLEm-HV2Y1C4F7Nrd97vQjg7RKtxIR1hccahuduwnnPutA/s640/benchmark.PNG&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
I am also trying to benchmark against this data, using &lt;a href=&quot;https://keras.io/&quot;&gt;keras&lt;/a&gt;. 

keras is a high level framework for building deep learning models, with selection of TensorFlow, Theano and CNTK for backend. It is easy to install and use. For my application, I used CNTK backend. You can refer to this &lt;a href=&quot;https://docs.microsoft.com/en-us/cognitive-toolkit/using-cntk-with-keras&quot; target=&quot;_blank&quot;&gt;article&lt;/a&gt; on its installation.&lt;br /&gt;
&lt;br /&gt;
Here, I will benchmark two models. One is a MLP with layer structure of 256-512-100-10, and the other one is a VGG-like CNN. Code is available at my github: &lt;a href=&quot;https://github.com/xieliaing/keras-practice/tree/master/fashion-mnist&quot;&gt;https://github.com/xieliaing/keras-practice/tree/master/fashion-mnist&lt;/a&gt;&lt;br /&gt;
&lt;br /&gt;
The first model achieved accuracy of [0.89, 0.90] on testing data after 100 epochs, while the latter achieved accuracy of &amp;gt;0.94 on testing data after 45 epochs.

First, read in the Fashion-MNIST data:
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
import numpy as np
import io, gzip, requests
train_image_url = &quot;http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-images-idx3-ubyte.gz&quot;
train_label_url = &quot;http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-labels-idx1-ubyte.gz&quot;
test_image_url = &quot;http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-images-idx3-ubyte.gz&quot;
test_label_url = &quot;http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-labels-idx1-ubyte.gz&quot;

def readRemoteGZipFile(url, isLabel=True):
    response=requests.get(url, stream=True)
    gzip_content = response.content
    fObj = io.BytesIO(gzip_content)
    content = gzip.GzipFile(fileobj=fObj).read()
    if isLabel:
        offset=8
    else:
        offset=16
    result = np.frombuffer(content, dtype=np.uint8, offset=offset)    
    return(result)

train_labels = readRemoteGZipFile(train_label_url, isLabel=True)
train_images_raw = readRemoteGZipFile(train_image_url, isLabel=False)

test_labels = readRemoteGZipFile(test_label_url, isLabel=True)
test_images_raw = readRemoteGZipFile(test_image_url, isLabel=False)

train_images = train_images_raw.reshape(len(train_labels), 784)
test_images = test_images_raw.reshape(len(test_labels), 784)
&lt;/code&gt;&lt;/pre&gt;
Let&#39;s first visual it using tSNE. tSNE is said to be the most effective dimension reduction tool.This plot function is borrowed from &lt;a href=&quot;http://scikit-learn.org/stable/auto_examples/manifold/plot_lle_digits.html#sphx-glr-auto-examples-manifold-plot-lle-digits-py&quot;&gt;sklearn example&lt;/a&gt;.

&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
from sklearn import manifold
from time import time
import matplotlib.pyplot as plt
from matplotlib import offsetbox
plt.rcParams[&#39;figure.figsize&#39;]=(20, 10)
# Scale and visualize the embedding vectors
def plot_embedding(X, Image, Y, title=None):
    x_min, x_max = np.min(X, 0), np.max(X, 0)
    X = (X - x_min) / (x_max - x_min)

    plt.figure()
    ax = plt.subplot(111)
    for i in range(X.shape[0]):
        plt.text(X[i, 0], X[i, 1], str(Y[i]),
                 color=plt.cm.Set1(Y[i] / 10.),
                 fontdict={&#39;weight&#39;: &#39;bold&#39;, &#39;size&#39;: 9})

    if hasattr(offsetbox, &#39;AnnotationBbox&#39;):
        # only print thumbnails with matplotlib &amp;gt; 1.0
        shown_images = np.array([[1., 1.]])  # just something big
        for i in range(X.shape[0]):
            dist = np.sum((X[i] - shown_images) ** 2, 1)
            if np.min(dist) &amp;lt; 4e-3:
                # don&#39;t show points that are too close
                continue
            shown_images = np.r_[shown_images, [X[i]]]
            imagebox = offsetbox.AnnotationBbox(
                offsetbox.OffsetImage(Image[i], cmap=plt.cm.gray_r),
                X[i])
            ax.add_artist(imagebox)
    plt.xticks([]), plt.yticks([])
    if title is not None:
        plt.title(title)
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
tSNE is very computationally expensive, so for impatient people like me, I used 1000 samples for a quick run. If your PC is fast enough and have time, you can run tSNE against the full dataset.

&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
sampleSize=1000
samples=np.random.choice(range(len(Y_train)), size=sampleSize)
tsne = manifold.TSNE(n_components=2, init=&#39;pca&#39;, random_state=0)
t0 = time()
sample_images = train_images[samples]
sample_targets = train_labels[samples]
X_tsne = tsne.fit_transform(sample_images)
t1 = time()
plot_embedding(X_tsne, sample_images.reshape(sample_targets.shape[0], 28, 28), sample_targets,
               &quot;t-SNE embedding of the digits (time %.2fs)&quot; %
               (t1 - t0))
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjxmOZcwQ7r5jyHpGiHgvXh-2_tRUzwp4il4Zjehyphenhyphenq_Uri2AD7-Ng5pBVC5yHlD0D7tMphEvpCy0RTtr5wL8ZaTlWNxKLbvAQ1tclo7P_gyMLdatK-49uJDyO3hMasR7Is3emGy0g/s1600/tSNE.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;577&quot; data-original-width=&quot;1140&quot; height=&quot;324&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjxmOZcwQ7r5jyHpGiHgvXh-2_tRUzwp4il4Zjehyphenhyphenq_Uri2AD7-Ng5pBVC5yHlD0D7tMphEvpCy0RTtr5wL8ZaTlWNxKLbvAQ1tclo7P_gyMLdatK-49uJDyO3hMasR7Is3emGy0g/s640/tSNE.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
We see that several features, including mass size, split on bottom and semetricity, etc, separate the categories. Deep learning excels here because you don&#39;t have to manually engineering the features but let the algorithm extracts those.&lt;br /&gt;
&lt;br /&gt;
In order to build your own networks, we first import some libraries
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
from keras.models import Sequential
from keras.layers.convolutional import Conv2D, MaxPooling2D, AveragePooling2D
from keras.layers.advanced_activations import LeakyReLU
from keras.layers import Activation
&lt;/code&gt;&lt;/pre&gt;
We also do standard data preprocessing:
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
X_train = train_images.reshape(train_images.shape[0], 28, 28, 1).astype(&#39;float32&#39;)
X_test = test_images.reshape(test_images.shape[0], 28, 28, 1).astype(&#39;float32&#39;)

X_train /= 255
X_test /= 255

X_train -= 0.5
X_test -= 0.5

X_train *= 2.
X_test *= 2.

Y_train = train_labels
Y_test = test_labels
Y_train2 = keras.utils.to_categorical(Y_train).astype(&#39;float32&#39;)
Y_test2 = keras.utils.to_categorical(Y_test).astype(&#39;float32&#39;)
&lt;/code&gt;&lt;/pre&gt;
Here is the simple MLP implemented in keras:

&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
mlp = Sequential()
mlp.add(Dense(256, input_shape=(784,)))
mlp.add(LeakyReLU())
mlp.add(Dropout(0.4))
mlp.add(Dense(512))
mlp.add(LeakyReLU())
mlp.add(Dropout(0.4))
mlp.add(Dense(100))
mlp.add(LeakyReLU())           
mlp.add(Dropout(0.5))
mlp.add(Dense(10, activation=&#39;softmax&#39;))
mlp.compile(loss=&#39;categorical_crossentropy&#39;, optimizer=&#39;adam&#39;, metrics=[&#39;accuracy&#39;])
mlp.summary()
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
This model achieved almost 90% accuracy on test dataset at about 100 epochs.

Now, let&#39;s build a VGG-like CNN model. We use an architecture that is similar to VGG but still very different. Because the figure data is small, if we use original VGG architecture, it is very likely to overfit and won&#39;t perform very well in testing data which is observed in publically submitted benchmarks listed above. To build such a model in keras is very natural and easy:&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
num_classes = len(set(Y_train))
model3=Sequential()
model3.add(Conv2D(filters=32, kernel_size=(3, 3), padding=&quot;same&quot;, 
          input_shape=X_train.shape[1:], activation=&#39;relu&#39;))
model3.add(Conv2D(filters=64, kernel_size=(3, 3), padding=&quot;same&quot;, activation=&#39;relu&#39;))
model3.add(MaxPooling2D(pool_size=(2, 2)))
model3.add(Dropout(0.5))
model3.add(Conv2D(filters=128, kernel_size=(3, 3), padding=&quot;same&quot;, activation=&#39;relu&#39;))
model3.add(Conv2D(filters=256, kernel_size=(3, 3), padding=&quot;valid&quot;, activation=&#39;relu&#39;))
model3.add(MaxPooling2D(pool_size=(3, 3)))
model3.add(Dropout(0.5))
model3.add(Flatten())
model3.add(Dense(256))
model3.add(LeakyReLU())
model3.add(Dropout(0.5))
model3.add(Dense(256))
model3.add(LeakyReLU())
#model2.add(Dropout(0.5))
model3.add(Dense(num_classes, activation=&#39;softmax&#39;))
model3.compile(loss=&#39;categorical_crossentropy&#39;, optimizer=&#39;adam&#39;, metrics=[&#39;accuracy&#39;])
model3.summary()
&lt;/code&gt;&lt;/pre&gt;
This model has 1.5million parameters. We can call &#39;fit&#39; method to train the model:
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
model3_fit=model3.fit(X_train, Y_train2, validation_data = (X_test, Y_test2), epochs=50, verbose=1, batch_size=500)
&lt;/code&gt;&lt;/pre&gt;
After 40 epochs, this model archieves accuracy of 0.94 on testing data.Obviously, there is also overfitting problem for this model. We will address this issue later.&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBJ3X03TPoSGdnrgVRUX9Py-n_Zerh56-cPyqvMA3VUAeJZEUXx8QxaT8R2iK27-FL0D14CHLxhsOKJPdrfxqbgqfH3-A1hYpHMPMLOJ2XBX88k_mrfOMtV46xH_thraDicDR-ZA/s1600/VGG-like+epoch+hist.PNG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;723&quot; data-original-width=&quot;824&quot; height=&quot;560&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBJ3X03TPoSGdnrgVRUX9Py-n_Zerh56-cPyqvMA3VUAeJZEUXx8QxaT8R2iK27-FL0D14CHLxhsOKJPdrfxqbgqfH3-A1hYpHMPMLOJ2XBX88k_mrfOMtV46xH_thraDicDR-ZA/s640/VGG-like+epoch+hist.PNG&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGokBGm6xAm9JVueg6S4jqv3e9x1neviFwjwguHpfPlKkqelWgzuOsZQ7USAmovDIeNwKevezlk_x58FRcIUQL84CcAeFO1YY7COG_2HCCZwQadZWsK9FW4U0jkfNaHtVkzhyGbQ/s1600/ModelPerformance.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;578&quot; data-original-width=&quot;1163&quot; height=&quot;318&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGokBGm6xAm9JVueg6S4jqv3e9x1neviFwjwguHpfPlKkqelWgzuOsZQ7USAmovDIeNwKevezlk_x58FRcIUQL84CcAeFO1YY7COG_2HCCZwQadZWsK9FW4U0jkfNaHtVkzhyGbQ/s640/ModelPerformance.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/3998091181104122036/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/3998091181104122036' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/3998091181104122036'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/3998091181104122036'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2017/09/a-vgg-like-cnn-for-fashion-mnist-with.html' title='A VGG-like CNN in keras for Fashion-MNIST with 94% accuracy'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiq-W6_UdsMgEOQV0et7tbbLI9D3bgrxmRvDBG4Orw4WaddfMkezPwpl38U5Q__QqLV84mQQngMS29CimMGV7mvRKnfIWFCdVkuZ-85Sr8QQNoSVdH2bVXhW5sUxhiIwl0btAGzjg/s72-c/fashion-mnist-sprite.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-8884517825725074243</id><published>2017-09-03T00:32:00.001-07:00</published><updated>2017-09-08T09:41:07.938-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="deep learning"/><category scheme="http://www.blogger.com/atom/ns#" term="keras"/><title type='text'>如何零基础使用KERAS搭建实用深度模型</title><content type='html'>在这篇小文章中，我们将简要讨论如何使用&lt;a href=&quot;https://keras.io/&quot; target=&quot;_blank&quot;&gt;KERAS&lt;/a&gt;这个现在最新的深度学习框架来构造实用的深度学习模型。&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;深度学习是目前最热门的高级分析技术之一，在很多方面表现出了超越传统机器学习方法的有效性。但是在常用的&lt;a href=&quot;https://www.tensorflow.org/&quot; target=&quot;_blank&quot;&gt;TensorFlow&lt;/a&gt;，&lt;a href=&quot;https://docs.microsoft.com/en-us/cognitive-toolkit/&quot; target=&quot;_blank&quot;&gt;CNTK&lt;/a&gt;，&lt;a href=&quot;http://www.deeplearning.net/software/theano/&quot; target=&quot;_blank&quot;&gt;Theano&lt;/a&gt;等计算环境中实现不同的深度学习模型仍然需要耗费很多时间来编写程序。KERAS的出现提供了一个高度抽象的环境来搭建深度学习模型，特别是其简单易用，跟网络结构一一对应的特点使得其迅速在数据科学家这个使用人群中流行起来。&lt;br /&gt;
&lt;br /&gt;
## 什么是KERAS&lt;br /&gt;
&lt;br /&gt;
KEARS是Google工程师François Chollet为主创人员，基于Python开发和维护的一个抽象的神经网络建模环境，提供了一系列的API供用户调用构造自己的深度学习网络。KERAS的出发点就是为用户提供一个能够快速实现模型的手段，从而缩短建模迭代的时间，加快模型试验的频率。用KERAS开发者的话说，就是要做好的科研必须尽可能地缩短从想法到实现结果的时间。在业界工作中这也是成功的关键要素之一。&lt;br /&gt;
&lt;br /&gt;
相比较于常见的深度学习环境，比如TensorFlow，CNTK，Theano，Caffe等，KERAS有以下几个不同：&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;1. 设计初衷就是方便以模块化地方式快速构造深度学习模型的原型；&lt;br /&gt;
&lt;br /&gt;
2. 可以很方便地在CPU和GPU之间切换&lt;br /&gt;
&lt;br /&gt;
3. KERAS本身只是描述模型的环境，其计算平台目前依赖于TensorFlow，CNTK和Theano这三种，以后会拓展到其他流行的计算平台上，比如mxNet等；&lt;br /&gt;
&lt;br /&gt;
4. KERAS的拓展性既可以通过自定义KERAS里的激活函数或者损失函数等能自定义的部分进行，也可以通过引用对应的计算平台的自定义部分进行，具有一定的灵活性；&lt;br /&gt;
&lt;br /&gt;
跟这些流行的计算平台一样，KERAS也支持常见的深度学习模型，比如卷积神经网络，循环神经网络以及二者的组合等。&lt;br /&gt;
&lt;br /&gt;
使用KERAS构造深度神经网络有一系列相对固定的步骤：&lt;br /&gt;
&lt;br /&gt;
1. 首先要将原始数据处理成KERAS的API能够接受的格式，一般是一个张量的形式，通常在维度上表示为（批量数，[单一样本对应张量的维度]）。这里[单一样本对应张量的维度] 是一个通用的说法，对应于不同类型的模型，数据有不同的要求。
通常，如果是一个简单的全链接模型，则单一样本对应张量的维度就是特征个数；
如果是一维的时间序列数据，并要用循环神经网络模型训练的话，则单一样本对应张量的维度是时间步和每个时间步对应的回看序列长度；
如果输入数据是图像，并使用卷积神经网络模型进行训练，则单一样本张量对应图像的高，宽和色彩频道三个维度。但是如果是使用全连接模型训练图像数据，则单一样本对应张量是该图像扁化（Flatten）以后的向量长度，其为高，宽和色彩频道各个维度数量的乘积。一般卷积神经网络最靠近输出层的那层都设置一个全连接层，因此也需要扁化输入张量。&lt;br /&gt;
&lt;br /&gt;
2. 其次要构造需要的深度学习模型。这一步又分为模型的选择和模型的细化两个步骤：&lt;br /&gt;
&amp;nbsp;&amp;nbsp; -  选择模型的类型。KERAS里定义了两大类模型&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 1）序列模型(Sequential)；&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 2）通用模型(Model)。&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 序列模型指的是深度模型每一层之间都是前后序列关系，如下图所示：
 &lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgda8zT4OaoPlKQM3q8mmQKwKgKJ3V7IWGMU3nMSihHnvlsCQYGVFsBYmD296bo-vsUhqSspPbXpvEa1BFf0cW5JDH70R3aoOkgpg7YlkihE_Fem47LBjaT3R7EzOkD8V0FhtEzAQ/s1600/F1.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;214&quot; data-original-width=&quot;251&quot; height=&quot;545&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgda8zT4OaoPlKQM3q8mmQKwKgKJ3V7IWGMU3nMSihHnvlsCQYGVFsBYmD296bo-vsUhqSspPbXpvEa1BFf0cW5JDH70R3aoOkgpg7YlkihE_Fem47LBjaT3R7EzOkD8V0FhtEzAQ/s640/F1.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
Figure 1。MLP是一个典型的序列模型，[图片来源](http://article.sapub.org/10.5923.j.ajis.20120204.01.html)
可以看到从左到右，输入层到隐含层到输出层每一层之间都是前后依次相连的简单关系。这个简单的网络结构可以用三句KERAS命令实现：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
    model=Sequential()
    model.add(Dense(5, input_shape=(4,), activation=’sigmoid’))
    model.add(Dense(1, activation=’sigmoid’))
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
而通用模型则是对应更广义的模型，具备更大的灵活性。上面提到的序列模型也可以用通用模型来表达，这个我们在后一节详解。
当然通用模型更能用来描述层与层之间有较复杂关系的情况，比如非相邻的层之间进行连接，或者多个神经网络的合并等。比如我们可以使用通用模型进行矩阵分解：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
    user_in = Input(shape=(1,), dtype=&#39;int64&#39;, name=&#39;user_in&#39;)
    u = Embedding(n_users, n_factors, input_length=1)(user_in)
    movie_in = Input(shape=(1,), dtype=&#39;int64&#39;, name=&#39;movie_in&#39;)
    v = Embedding(n_movies, n_factors, input_length=1)(movie_in)
    x = merge([u, v], mode=&#39;dot&#39;)
    x = Flatten()(x)
    model = Model([user_in, movie_in], x)
    model.compile(Adam(0.001), loss=&#39;mse&#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
这里构造了一个基于矩阵分解的推荐系统的一个深度学习模型，其对应的网络结构如下图所示：&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiwkHhwiwFjpGxLGI9ciFIh0fg6vpOejuvgUkxTbsXOgQw0HbA89uwpua6BGsULBekjEKuNtOz7VnKNhGze6Zm0SZbP_73oAoVAixFTzqe_WAw_8LI-PhLvZG2t0MWa-LhWdeHpUA/s1600/F2.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;284&quot; data-original-width=&quot;425&quot; height=&quot;426&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiwkHhwiwFjpGxLGI9ciFIh0fg6vpOejuvgUkxTbsXOgQw0HbA89uwpua6BGsULBekjEKuNtOz7VnKNhGze6Zm0SZbP_73oAoVAixFTzqe_WAw_8LI-PhLvZG2t0MWa-LhWdeHpUA/s640/F2.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&amp;nbsp;Figure 2。矩阵分解的深度学习模型&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; - 细化模型的结构。其实上面已经展示了模型结构细化之后的情况。一般来说，确定了模型类型以后，其结构不外乎每层的类型是什么，是全连接层还是卷积层还是放弃（Dropout）层；每层的其他参数是什么，比如如果需要指定激活函数，那么使用什么样的激活函数，如果是卷积层，那么需要多少过滤器，每个过滤器的大小是怎样的？等等这些都可以通过设定不同的参数进行细化。&lt;br /&gt;
&lt;br /&gt;
3. 然后对模型进行编译，编译完成以后可以查看模型的基本信息，特别是参数的数量；&lt;br /&gt;
&lt;br /&gt;
4. 最后带入数据对模型进行拟合。一般来讲，如果数据是静态的张量数据，通过使用fit方法。如果数据特别大，可是使用可迭代的data generator对象，并使用fit_generator方法来拟合。&lt;br /&gt;
&lt;br /&gt;
## KERAS和深度学习模型的对应关系&lt;br /&gt;
&lt;br /&gt;
KERAS既然是开发出来快速构造深度学习模型的工具，那么它的API和深度学习模型的要素都有很强的对应关系。

正如上面所说，目前的深度学习模型都可以纳入序列模型或者通用模型的，那么我们用图示的方式来表示这个对应关系，方便读者理解。这里网络图为了方便与按行排列的代码对应，对每一层都进行了标注。

下图展示的是一个典型的全连接序列模型：
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhsDkqrO89ehvVJCOQhfF9u4ComacKoxtRejup5CfBehqcmyJ-ZHqrb2anNNO2HB1LPoZiOSNNtL_kMovoelh1dY0WTnCpsM3XQ59f4zkoXk5Cl9T4Hhp4qBy61D2m8oj3e8qdoyA/s1600/F3.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;674&quot; data-original-width=&quot;1393&quot; height=&quot;308&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhsDkqrO89ehvVJCOQhfF9u4ComacKoxtRejup5CfBehqcmyJ-ZHqrb2anNNO2HB1LPoZiOSNNtL_kMovoelh1dY0WTnCpsM3XQ59f4zkoXk5Cl9T4Hhp4qBy61D2m8oj3e8qdoyA/s640/F3.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
Figure 3。全连接序列模型，修改自[博客](http://www.samyzaf.com/ML/pima/pima.html)&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;这个序列模型可以使用如下的KERAS命令快速搭建：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
Model = Sequential()
Model.add(Dense(10, activation=’sigmoid’, 
                   input_shape=(8, ))             【隐含层1+输入层】
Model.add(Dense(8, activation=’relu’))          【隐含层2】
Model.add(Dense(10, activation=’relu’))         【隐含层3】
Model.add(Dense(5, activation=’softmax’))       【输出层】
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
上面的序列模型也可以用通用模型的API描述的结果，其与图中的网络结构有更强的对应关系：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
x = Input(shape=(8,))                          【输入层】
b = Dense(10, activation=’sigmoid’)(x)       【隐含层1】
c = Dense(8, activation=’relu’)(b)           【隐含层2】
d = Dense(10, activation=’relu’)(c )         【隐含层3】
out = Dense(5, activation=’softmax’)(d)      【输出层】
model = Model(inputs=x, outputs=out)
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
上面也举了另外的比较复杂的例子。在后面的具体案例中，我们也会强调网络结构和对应的KERAS命令，使读者能建立起较强的联系。

##使用KERAS构造深度推荐系统

推荐系统是机器学习最广泛的应用领域之一，大家熟悉的亚马逊、迪士尼、谷歌、Netflix 等公司都在网页上有其推荐系统的界面，帮助用户更快、更方便地从海量信息中找到有价值的信息。比如亚马逊（&lt;a href=&quot;http://www.amazon.com/&quot;&gt;www.amazon.com&lt;/a&gt;）会给你推荐书、音乐等，迪士尼（&lt;a href=&quot;http://video.disney.com/&quot;&gt;video.disney.com&lt;/a&gt;）给你推荐最喜欢的卡通人物和迪士尼电影，谷歌搜索更不用说了， Google Play、 Youtube 等也有自己的推荐引擎、推荐视频和应用等。下面是我登陆亚马逊之后的一个推荐页面，可见我之前应该是购买了咖啡机，所以会有相关的产品推荐出来。&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOT0g74MKsIva_ts6uVSYcDrLty1Pijc5TPb-vGgJ0VpPC1l8RpfYz5OBAiYWM5upSsn4ow1jaQUlVFkZeoFEAoUcCTvxSswTaTBuuMq22nWkav1DzAC8_ElQZFMUIN939Hd8vfQ/s1600/F4.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;251&quot; data-original-width=&quot;1136&quot; height=&quot;139&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOT0g74MKsIva_ts6uVSYcDrLty1Pijc5TPb-vGgJ0VpPC1l8RpfYz5OBAiYWM5upSsn4ow1jaQUlVFkZeoFEAoUcCTvxSswTaTBuuMq22nWkav1DzAC8_ElQZFMUIN939Hd8vfQ/s640/F4.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&amp;nbsp;Figure 4。亚马逊的推荐页面局部&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;推荐系统的最终目的是从百万甚至上亿内容或者商品中把有用的东西高效地显示给用户，这样可以为用户节省很多自行查询的时间，也可以提示用户可能忽略的内容或商品，使用户更有黏性，更愿意花时间待在网站上，从而使商家可以从内容或者商品中赚取更多的利润，即使流量本身也会使商家从广告中受益。&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;传统上，推荐系统是基于矩阵分解的协同过滤算法，前面也展示了这样的一个简单模型。下面我们着重介绍深度学习推荐系统。这个模型除了能将用户和可选产品联系起来意外，还能将其他辅助数据，比如用户年龄，地区，上网设备以及各种产品属性，联系起来。&lt;br /&gt;
&lt;br /&gt;
这里通过嵌入（Embedding）这种技术将不同的信息串在一起作为输入层，再继续搭建不同的神经网络模型，最后一层用预测评分作为输出层。虽然这里的数据只有用户编码和电影产品编码，但是这样的结构可以拓展到包含其他相关数据。下图展示了这样的一个深度模型的结构示意图：&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjH-b6R48osRaPEz1KiZuPVTLrnn6REKoAYVu6R5hjyirKFbM2qLVxLqJSFkr8RuE0RRa7eWeSNmh5_PyzUyj2rbgsnpfllIlg_7U6i2hSJMLKWBQkhKWUPPG09BtxYewUm7KDNcw/s1600/F5.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;415&quot; data-original-width=&quot;369&quot; height=&quot;640&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjH-b6R48osRaPEz1KiZuPVTLrnn6REKoAYVu6R5hjyirKFbM2qLVxLqJSFkr8RuE0RRa7eWeSNmh5_PyzUyj2rbgsnpfllIlg_7U6i2hSJMLKWBQkhKWUPPG09BtxYewUm7KDNcw/s640/F5.png&quot; width=&quot;568&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
Figure 5。深度模型&lt;br /&gt;
&lt;br /&gt;
有了这个示意图，我们就可以很方便地用KERAS依次构造。这里我们假设已经将用户和电影产品做了按照One Hot编码形式组织好了。&lt;br /&gt;
&lt;br /&gt;
首先用嵌入层对用户和电影进行嵌入映射：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
k = 128
model1 = Sequential()
model1.add(Embedding(n_users + 1, k, input_length = 1))
model1.add(Reshape((k,)))
model2 = Sequential()
model2.add(Embedding(n_movies + 1, k, input_length = 1))
model2.add(Reshape((k,)))
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
这里的k是映射到的空间的维度。在一般的业务系统中我们可能有上百万的用户和产品，经过嵌入映射到128维的实数域上以后显著地降低了整个系统的维度和大小。

以上几句命令实现了上图从最低下到“用户嵌入”和“电影嵌入”这一阶段的编程。&lt;br /&gt;
&lt;br /&gt;
其次，我们需要用第三个神经网络把前面的两个嵌入网络映射所得到的向量叠加在一起：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
model = Sequential()
model.add(Merge([model1, model2], mode = &#39;concat&#39;))
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
至此完成了到第一个粗箭头的网络构造。两个网络已经合并为一个网络。
下面的命令依次完成“隐含层128”和“隐含层32”的构造：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
model.add(Dropout(0.2))
model.add(Dense(k, activation = &#39;relu&#39;))
model.add(Dropout(0.5))
model.add(Dense(int(k/4), activation = &#39;relu&#39;))
model.add(Dropout(0.5))
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
下面继续构造“隐含层8”：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
model.add(Dense(int(k/16), activation = &#39;relu&#39;))
model.add(Dropout(0.5))
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
隐含层构造完毕之后，需要构造输出层。因为是预测连续变量评分，最后一层直接上线性变化：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
model.add(Dense(1, activation = &#39;linear&#39;))
&lt;/code&gt;&lt;/pre&gt;
至此，模型构造完毕，可以编译了：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
model.compile(loss = &#39;mse&#39;, optimizer = &quot;adam&quot;)
&lt;/code&gt;&lt;/pre&gt;
这里使用了均方差（MSE）作为损失函数，并使用了ADAM优化算法。

下面，为了能训练模型，需要将数据构造为[users, movies]的形式：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
users = ratings[&#39;user_id&#39;].values
movies = ratings[&#39;movie_id&#39;].values
X_train = [users, movies]
&lt;/code&gt;&lt;/pre&gt;
最后训练模型：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
model.fit(X_train, y_train, batch_size = 100, epochs = 50)
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
使用movielens的用户观看电影评分数据进行训练和验证，我们发现这个模型的误差在0.8226左右，大约一个评分等级不到。&lt;br /&gt;
&lt;br /&gt;
即使这样一个简单的模型，效果还是比较好的。如果进一步优化结构，或者引入其他信息，误差还可以进一步降低。&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;##使用KERAS构造图像识别系统&lt;br /&gt;
&lt;br /&gt;
图像识别是深度学习最典型的应用之一。关于深度学习的图像识别可以追溯很长的历史，其中最具有代表性的例子是手写字体识别和图片识别。手写字体识别主要是用机器正确区别手写体数字 0~9。银行支票上的手写体识别技术就是基于这个技术。图片识别的代表作就是 ImageNet。这个比赛需要团队识别图片中的动物或者物体，把它们正确地分到一千个类别中的其中一个。

图像识别有很多种技术可以实现，目前最主流的技术是深度神经网络，其中尤以卷积神经网络（CNN）最为出名。卷积神经网络（见图1）是一种自动化特征提取的机器学习模型。从数学的角度看，任何一张图片都可以对应到 224 × 224 × 3 或者 32 × 32 × 3 等三维向量，这取决于像素。我们的目标是把这个三维向量（又被称为张量）映射到 N个类别中的一类。神经网络就是建立了这样一个映射关系，或者称为函数。它通过建立网状结构，辅以矩阵的加、乘等运算，最后输出每个图像属于每个类别的概率，并且取概率最高的作为我们的决策依据。 下面是一个典型的序列卷积神经网络模型的结构：&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgjjTL8BPASWVQ28nWEb6T9pd-MlEp4Yd_sc626jyY_cwmKtA_U2sDItii-8CiZb2X35RvIj7mr3w0-DM8EI6qVMqbmwttOSbsX986mxLSXTknylhFoO-LdFF02AqO2Vko1aKrdrA/s1600/F6.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;261&quot; data-original-width=&quot;623&quot; height=&quot;267&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgjjTL8BPASWVQ28nWEb6T9pd-MlEp4Yd_sc626jyY_cwmKtA_U2sDItii-8CiZb2X35RvIj7mr3w0-DM8EI6qVMqbmwttOSbsX986mxLSXTknylhFoO-LdFF02AqO2Vko1aKrdrA/s640/F6.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
Figure 6。卷积神经网络结构。来源于CNTK教程&lt;br /&gt;
&lt;br /&gt;
上面这个网络依次展示了卷积网络模型的主要要素：&lt;br /&gt;
- 输入层的图像；&lt;br /&gt;
- 卷积操作；&lt;br /&gt;
- 激活函数的应用；&lt;br /&gt;
- 池化操作；&lt;br /&gt;
- 将数据展平（Flatten），为输出到全连接层做准备；&lt;br /&gt;
- 全连接层准备输出；&lt;br /&gt;
- softmax应用于分类问题的全连接层作为输出层。&lt;br /&gt;
&lt;br /&gt;
下面详细介绍一下在KERAS中如何对应地进行编程处理。&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;- 首先，这是一个序列模型，因此先要声明一个序列模型的对象：&lt;br /&gt;
model.=sequential()&lt;br /&gt;
&lt;br /&gt;
- 卷积是应用一个局部的过滤器到原始数据的过程，比如下图就展示了一个3x3的过滤器应用在一个7x7的图像上过程。假设在当前步，这个过滤器的权重经过学习得到如图所示的权重结果，在当前步继续进行卷积操作就是将这个3x3的过滤器从左上角每次要么向右要么向下移动一格，和对应的图像局部的3x3的区域进行依元素点乘求和得到卷积的结果。因为依次移动，到最边上的时候过滤器会超出图像的边界，一般会将这些对应的卷积结果删除，从而卷积后的张量维度会少于原始图像。比如这个例子中原图为7x7，使用一个3x3的过滤器卷积之后最后两列和两行进行卷积的时候会使过滤器超过边界，因此最后的卷积结果是一个5x5的图像。这里可以使用多个过滤器，每个过滤器应用一次，每次应用产生的卷积结果构成隐含层的一层。比如采用16个过滤器，如果不删除边界的过滤结果，则得到新的[7x7x16]的张量。&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHwtWzbj35jup5Fj7QOwTendeeApeWMfx-TGAY3-qahuKvpZ24Gr6MgOBsls-peM1bPbQ-pIeUnlbfQnCg9AjhDtmAA4_AQevoPTiepcYieGI6je8xwJpoKfzu9FZfz9IlM_gwUg/s1600/F7.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;352&quot; data-original-width=&quot;782&quot; height=&quot;288&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHwtWzbj35jup5Fj7QOwTendeeApeWMfx-TGAY3-qahuKvpZ24Gr6MgOBsls-peM1bPbQ-pIeUnlbfQnCg9AjhDtmAA4_AQevoPTiepcYieGI6je8xwJpoKfzu9FZfz9IlM_gwUg/s640/F7.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
Figure 7．卷积演示，来源于CNTK教程&lt;br /&gt;
&lt;br /&gt;
在KERAS里，对于图像这种二维数据，一般使用Conv2D这个二维卷积层。Conv2D有几个必备的参数：&lt;br /&gt;
1. 首先指定过滤器数量 filter，是一个整数；&lt;br /&gt;
2. 第二是要指定二维过滤器的大小，比如(3,3)；&lt;br /&gt;
3. 第三要指定步长，即延某轴移动的时候是依次移动一个像素还是多个像素，默认为1；&lt;br /&gt;
4. 第四要指定补齐策略padding，即是否要将在边界的卷积结果去掉。如果值为”same”，则不去掉，卷积结果和输入图像有同样的高和宽；如果值为”valid”，则不会处理过滤器会超出边界的像素。&lt;br /&gt;
5. 最后，如果卷积层是第一层，那么还需要指明输入数据的维度input\_shape。因为一般用TensorFlow或者CNTK做后台，输入数据要求是channel_last，因此输入的原始维度为[样本量，高，宽，频道]，那么这里的维度只需要去掉样本量即可，即为[高，宽，频道数]，一般用X.shape[1:]即可得到。
&lt;br /&gt;
&lt;ul&gt;
对于上面的例子，KERAS里的典型写法是：
&lt;/ul&gt;
&lt;ul&gt;
&lt;pre&gt;&lt;code&gt;
model.add(Conv2D(filters=16, kernel_size=(3, 3),
                  strides=1, padding=”valid”, 
                  input_shape=xtrain.shape[1:]))
&lt;/code&gt;&lt;/pre&gt;
&lt;/ul&gt;
- 再次要添加激活层引入激活函数，通常是一个非线性的函数。激活函数既可以通过在Conv2D里面指定activation=参数引入，也可以通过单独添加Activation层引入。&lt;br /&gt;
&lt;br /&gt;
卷积神经网络常用的激活函数是Rectified Linear Unit， 简称relu。该函数其实就是max(0, x)，在层次较深的网络中比以前常用的取值区间在（0，1）或者（-1， 1）之间的sigmoid类激活函数效果好，因为不存在梯度消失的问题。
&lt;br /&gt;
&lt;ul&gt;
如果是通过参数，则上面的代码改写为：
&lt;/ul&gt;
&lt;ul&gt;
&lt;pre&gt;&lt;code&gt;
model.add(Conv2D(filters=16, kernel\_size=(3, 3),
                 strides=1, padding=”valid”, 
                 activation=’relu’)
&lt;/code&gt;&lt;/pre&gt;
&lt;/ul&gt;
&lt;ul&gt;
如果通过添加激活层引入，则在上面的代码后添加：
&lt;/ul&gt;
&lt;ul&gt;
&lt;pre&gt;&lt;code&gt;
model.add(Activation(‘relu’))
&lt;/code&gt;&lt;/pre&gt;
&lt;/ul&gt;
- 然后进行的池化操作是在卷积神经网络中对图像特征的一种处理，通常在卷积操作和激活函数之后进行。池化操作是将原有输入按照一定大小切分成互不交叉的局部区域，目的是为了计算特征在局部的充分统计量，从而降低总体的特征数量，防止过度拟合和减少计算量。下图展示了最大池化方法的应用。在一个6x6的图像上应用3x3的池化操作，将原输入矩阵切割为不相交叉的2x2区域，每个区域的取值是对应原输入局部的最大值。&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiV8czvNGEe9P3VbvPsGwz9J_FzW1jUm7A3C8Mw8jOdRzBn0XJ0-gN7c6BjJxju_krCWgeHBYlNYgFQqNZ4TE601Iog-1ZuUgCsKZB80-MlNW1hxgcKTa-V05uf3fEcVT9MsPplZg/s1600/F8.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;197&quot; data-original-width=&quot;447&quot; height=&quot;282&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiV8czvNGEe9P3VbvPsGwz9J_FzW1jUm7A3C8Mw8jOdRzBn0XJ0-gN7c6BjJxju_krCWgeHBYlNYgFQqNZ4TE601Iog-1ZuUgCsKZB80-MlNW1hxgcKTa-V05uf3fEcVT9MsPplZg/s640/F8.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
Figure 8。最大池化操作
对应于图像的最大池化层通过MaxPooling2D，KERAS也支持平均池化层，区别在于取对应局部的平均值作为池化后结果，方法为AveragePooling2D。对应上面的例子，KERAS的命令如下：
&lt;br /&gt;
&lt;ul&gt;
&lt;pre&gt;&lt;code&gt;
model.add(MaxPooling2D(pool_size=(3, 3))
&lt;/code&gt;&lt;/pre&gt;
&lt;/ul&gt;
- 为了输出到全连接层，先要对数据进行展平（Flatten）。这是因为全连接层只处理包含样本数在内一共二维的数据，要求第一维是样本数，第二维是所有特征的个数。因此对于一个包含2000个样本，每个样本是28x28x3的小图像的数据，展平之后是一个2000x2352的矩阵，其中2352是28,28,3的乘积。在KERAS里进行展平非常简单，在上面的MaxPooling2D层之后添**model.add(Flatten())** 即可，KERAS能自己分析出输入和输出的维度。&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;- 在前面这些处理之后，但是在输出之前，通常会添加一个或者多个全连接层进一步处理数据。全连接层可以通过Dense指出，指明输出神经元个数和激活函数即可：&lt;br /&gt;
model.add(Dense(1000, activation=’relu’))&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;- 最后使用一个全连接层作为输出层，同样要求使用softmax激活函数，并使用跟输出类别同样多的神经元个数。比如识别0—9十个数字，那么就应该写作：&lt;br /&gt;
model.add(Dense(10, activation=’relu’))&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;把所有步骤组合到一起，我们就可以将图6显示的一个卷积神经网络模型相应地写为KERAS代码了：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
model=Sequential()
model.add(Conv2D(filters=32, kernel_size=(3, 3), 
          padding=&quot;same&quot;, 
          input_shape=X_train.shape[1:], 
          activation=&#39;relu&#39;))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(filters=64, kernel_size=(3, 3), padding=&quot;valid&quot;))
model.add(Activation(&#39;relu&#39;))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(128, activation=&#39;relu&#39;))
model.add(Dense(num_classes, activation=&#39;softmax&#39;))
&lt;/code&gt;&lt;/pre&gt;
是不是很 简单？

要训练这个模型非常简单。我们先编译这个模型并显示其关键信息：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
model.compile(loss=&#39;categorical_crossentropy&#39;, optimizer=&#39;adagrad&#39;, metrics=[&#39;accuracy&#39;])
model.summary()
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgCh58hXXLVVK6G-584mASgWbcUc-Cia-jaROeN8G-L9UEFDXIlDbCBS3Gv5NZb3wyAV75vWUj7d7foRi7Ifz6DRG2c6utyz-2mD68IXR_zO-6stz7Xl2sd4YhV2nBblYF9t9CmZQ/s1600/F9.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;389&quot; data-original-width=&quot;472&quot; height=&quot;526&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgCh58hXXLVVK6G-584mASgWbcUc-Cia-jaROeN8G-L9UEFDXIlDbCBS3Gv5NZb3wyAV75vWUj7d7foRi7Ifz6DRG2c6utyz-2mD68IXR_zO-6stz7Xl2sd4YhV2nBblYF9t9CmZQ/s640/F9.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&amp;nbsp;Figure 9。模型信息
我们看到这个模型一共有421642个参数，大多来自于倒数第二层的全连接层。
拟合这个模型也很简单：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
model.fit(X_train, y_train, 
          epochs=20, verbose=1, 
          batch_size=10, 
          validation_data = (X_test, y_test))
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
这里使用最标准的fit方法。其中指定几个核心参数：&lt;br /&gt;
&lt;br /&gt;
1. 训练用特征数据X\_train，&lt;br /&gt;
2. 训练用结果数据y\_train，&lt;br /&gt;
3. 迭代次数epochs，&lt;br /&gt;
4. 批量大小用batch\_size指定，&lt;br /&gt;
5. verbose表示显示训练过程的信息，如果值为0不显示任何中间信息，如果为1显示按批量拟合的进度，如果为2则显示拟合结果信息。&lt;br /&gt;
6. 另外读者还可以指定验证数据集，用validation_data这个参数表示，其包含一个tuple，第一个元素是验证用特征数据，第二个是验证用结果数据。&lt;br /&gt;
&lt;br /&gt;
下面我们使用这个模型训练识别0-9这十个数字，使用著名的MNIST数据。不过在训练之前还需要提及对数据的处理：&lt;br /&gt;
&lt;br /&gt;
* 首先将数据重塑为[样本数，高，宽，色彩通道数]的格式。这个可以通过numpy.reshape方法来实现。因为keras自带的MNIST数据已经是numpy的多维矩阵，并且是单色的，因此色彩通道数为1，因此数据重塑可以用下面的命令实现。读者可自行重塑验证用数据。
&lt;br /&gt;
&lt;ul&gt;
&lt;pre&gt;&lt;code&gt;
X_train = X_train.reshape(X_train.shape[0], 
                          X_train.shape[1], 
                          X_train.shape[2], 1).astype(float)
&lt;/code&gt;&lt;/pre&gt;
&lt;/ul&gt;
&lt;br /&gt;
* 其次，需要将数据的取值压缩到[0， 1]之间。这有利于拟合时用的随机梯度递降算法的稳定和收敛。这可以使用X_train /= 255.0 来实现。

。&lt;br /&gt;
&lt;br /&gt;
* 最后要将结果数据变为One Hot Code形式。KERAS提供了一个非常方便的方法to_categorical来实现这个功能：
&lt;br /&gt;
&lt;ul&gt;
&lt;pre&gt;&lt;code&gt;
y_train = keras.utils.to_categorical(y_train, len(set(y_train))) 
&lt;/code&gt;&lt;/pre&gt;
&lt;/ul&gt;
&lt;br /&gt;
下图的结果显示即使是这个非常简单的模型，其在验证数据上的预测准确率都是非常高的，达到了99.14%。&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhJs6BPvVSOtCd2YCr9AFMePpHkIpqRNALbVKqUt-WVRhr7g47nMvaNG60IbAebpceF68QGjj1ipAOPD61paKaz-u1esS43b6bED-a-ZEsDlZ2Zayvk9b8I8fosFcUFlAfRyY2Okw/s1600/F10.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;110&quot; data-original-width=&quot;825&quot; height=&quot;84&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhJs6BPvVSOtCd2YCr9AFMePpHkIpqRNALbVKqUt-WVRhr7g47nMvaNG60IbAebpceF68QGjj1ipAOPD61paKaz-u1esS43b6bED-a-ZEsDlZ2Zayvk9b8I8fosFcUFlAfRyY2Okw/s640/F10.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
Figure 10。简单卷积模型在MNIST数据上的拟合结果。&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;使用KERAS可以非常方便的构造自己的卷积神经网络，对于比较复杂的情况，也可以使用已经训练好的一些常见的高效模型，比如VGG16，Xception 等做迁移训练来拟合自己的数据。&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjfP5iwwopWOY7VfgRkJzWvkK6jMZ6dzBCD9qdjMwMKyfiuNqKAMVYvfioU9Pf4wjTbJ_wIaJfjJmX-Qlp0CZsdBzwdp85Yoi1YDy5-rDtigXhhe4zxoJcaBp9aNevVJIRPJTr2Dg/s1600/F11.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;276&quot; data-original-width=&quot;470&quot; height=&quot;374&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjfP5iwwopWOY7VfgRkJzWvkK6jMZ6dzBCD9qdjMwMKyfiuNqKAMVYvfioU9Pf4wjTbJ_wIaJfjJmX-Qlp0CZsdBzwdp85Yoi1YDy5-rDtigXhhe4zxoJcaBp9aNevVJIRPJTr2Dg/s640/F11.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
Figure 11． VGG16结构，来源于：https://www.cs.toronto.edu/~frossard/post/vgg16 &lt;br /&gt;
&lt;br /&gt;
上图是著名的VGG16模型的结构。根据刚才的学习结果，读者可以很快地模仿这个结构搭建自己的类似模型，但是KERAS在application库里已经提供了现成训练好的VGG16模型供读者读入使用。读者可以引用这个模型，将顶层去掉用自己的数据重新训练，但是底层的参数借用VGG16已经训练好的权重。这就是迁移学习的思路，可以大大降低需要训练的参数数量，加快新模型的开发。这里使用了通用模型以便在现有的VGG16模型上进行修改：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
model_vgg = VGG16(include_top = False, 
                  weights = &#39;imagenet&#39;, 
                  input_shape =(224,224,3))
model = Flatten(name = &#39;flatten&#39;)(model_vgg.output)
model = Dense(10, activation = &#39;softmax&#39;)(model)
model_vgg_mnist = Model(model_vgg.input, model, 
                        name = &#39;vgg16&#39;)
&lt;/code&gt;&lt;/pre&gt;
这里首先引用VGG16模型，但是通过参数include_top=False指定迁移除顶层以外的其余网络结构到自己的模型中。Weights=’imagenet’表示借用的权重是用ImageNet数据训练出来的额。
其次，通过函数方法在修改过的VGG16模型上构造一个新的扁平层用来连接新构造的全连接层，这个全连接层跟前面的模型没有区别。最后把修改过的VGG16模型和新的顶层叠加起来并赋予新的名字vgg16。这样就得到了一个基于VGG16的新模型。&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;## 使用KERAS构造时间序列预测模型&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;时间序列是在商业数据或者工程数据中经常出现的一种数据形式，是以时间为次序排列，用来描述和计量一系列过程或者行为的数据的统称。比如每天商店的收入流水或者某个工厂每小时的产品产出都是时间序列数据。一般研究的时间序列数据有两种类型。最常见的是跟踪单一的计量数据随时间变化的情况，即每个时间点上收集的数据是一个一维变量，这种是最常见的，通常的时间序列默认就是这种数据，也是本章研究的对象。另外一种时间序列数据是多个对象或者多个维度的计量数据随时间变化的情况，即每个时间点上收集的数据是一个多维变量，这种一般也被称为纵向数据（Longitudinal Data），但是不属于这里介绍的对象。&lt;br /&gt;
&lt;br /&gt;
在这里我们介绍如何搭建一个LSTM深度学习模型来对在汉口测量的长江每月流量数据进行预测建模。该数据来源于DataMarket 的[时间序列数据库](&lt;a href=&quot;https://datamarket.com/data/list/?q=provider:tsdl&quot;&gt;https://datamarket.com/data/list/?q=provider:tsdl&lt;/a&gt;)，由澳大利亚莫纳什大学的统计学教授Rob Hyndman 创建，收集了数十个公开的时间序列数据集。&lt;br /&gt;
&lt;br /&gt;
汉口长江月流量数据包含从 1865 年 1 月到 1978 年 12 月在汉口记录的长江每月的流量，总计 1368 个数据点。计量单位未知。&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgajZze46tlZq7fMno6tVnCH5RA8FYU5pdC-iV945ng4JAufQ8kYa5X_rGefMqOc3jYyFlCeL7X9_XNO4-oSTS4A3cft7-L0tPFbZ_w8LqAM9ZF2Gnigs0qgUV3k9vIrbVQxPrkfA/s1600/F12.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;526&quot; data-original-width=&quot;1054&quot; height=&quot;318&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgajZze46tlZq7fMno6tVnCH5RA8FYU5pdC-iV945ng4JAufQ8kYa5X_rGefMqOc3jYyFlCeL7X9_XNO4-oSTS4A3cft7-L0tPFbZ_w8LqAM9ZF2Gnigs0qgUV3k9vIrbVQxPrkfA/s640/F12.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&amp;nbsp;Figure 12。长江月度流量时间序列

在一般的时间序列建模中，都需要检验数据的平稳性，因为传统时间序列建模都是建立在平稳数据的假设之上。这个数据具备非常强的年度周期性。使用传统的统计技术建模的时候都需要侦测周期性，并消除之，对消除周期性之后的数据运用ARIMA模型建模。&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhE8fIsIFiB9UVx5sSOzzPFH5J2TCnHB8ygBRQAbC-_1Pd95Ab9ksz-pYd6_0JW_jWedW6AyLoJBlLm9fthegffPla9pWN3RsdWsmNnQOS39uxMJ15RiA-y9aV1plGyuBWaoegKLQ/s1600/F13.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;268&quot; data-original-width=&quot;505&quot; height=&quot;338&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhE8fIsIFiB9UVx5sSOzzPFH5J2TCnHB8ygBRQAbC-_1Pd95Ab9ksz-pYd6_0JW_jWedW6AyLoJBlLm9fthegffPla9pWN3RsdWsmNnQOS39uxMJ15RiA-y9aV1plGyuBWaoegKLQ/s640/F13.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&amp;nbsp;Figure 13。长江月度流量局部和移动平滑结果&lt;br /&gt;
&lt;br /&gt;
我们可以通过周期图谱法（Periodogram）来得到主要的周期幅度。在Python中可以使用scipy.signal.periodogram来得到周期图谱。在这里我们不是使用原始数据，而是使用原始数据的自相关函数的周期图谱来计算主要周期，这样可以抵消噪音的影响。对读入pandas DataFrame的原始数据ts运行下面的程序我们可以得到如下的周期图谱和计算得到的主要周期长度。
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
import statsmodels.api as sm
from statsmodels.tsa.stattools import acf
from scipy import signal
import peakutils as peak
acf_x, acf_ci = acf(ts, alpha=0.05, nlags=36)
fs=1
f, Pxx_den = signal.periodogram(acf_x, fs)
index = peak.indexes(Pxx_den)
cycle=(1/f[index[0]]).astype(int)
fig = plt.figure()
ax0 = fig.add_subplot(111)
plt.vlines(f, 0, Pxx_den)
plt.plot(f, Pxx_den, marker=&#39;o&#39;, linestyle=&#39;none&#39;, color=&#39;red&#39;)
plt.title(&quot;Identified Cycle of %i&quot; % (cycle))
plt.xlabel(&#39;frequency [Hz]&#39;)
plt.ylabel(&#39;PSD [V**2/Hz]&#39;)
plt.show()
print( index, f, Pxx_den)
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhihTYege1SUX2YE6k_UfMkZPwEiYz9eo3ZPLzG7ynEhS0Ly-T2nkx0atc-2zaMdgSa3Uy4qy5HGfOG2-8JcRjKo35DXXnWUwjDpXly7Xyzb6szH-K1rRPLQH6EePbNNU3IyTIhig/s1600/F14.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;552&quot; data-original-width=&quot;1059&quot; height=&quot;332&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhihTYege1SUX2YE6k_UfMkZPwEiYz9eo3ZPLzG7ynEhS0Ly-T2nkx0atc-2zaMdgSa3Uy4qy5HGfOG2-8JcRjKo35DXXnWUwjDpXly7Xyzb6szH-K1rRPLQH6EePbNNU3IyTIhig/s640/F14.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
Figure 14。周期图谱

很明显有一个周期为 12 个月的季节性。&lt;br /&gt;
&lt;br /&gt;
虽然考虑到这个数据的本质是长江水文资料， 12 个月的周期是非常自然的预期，但是这个方法展示了对 ACF 序列运用周期图法（periodogram）找季节性周期的可靠性。在传统方法里，这里需要通过取间隔为12 的差分来消除周期性，得到一个尽可能平稳的时间序列，进而采用ARIMA模型建模。在Python里，单周期的时间序列数据，知道周期的长度以后可以直接使用季节性ARIMA模型（SARIMA）来训练。

但是在使用循环神经网络模型的时候我们不用考虑这些情况，可以直接使用长短记忆模型。此外，在使用LSTM这种序列模型的时候在使用LSTM对这种单一时间序列进行建模的时候，一般通过一下步骤：&lt;br /&gt;
&lt;br /&gt;
1. 将数据标准化为[0，1]区间。&lt;br /&gt;
2. 按照LSTM的要求，将输入数据组织为[样本数，时间步，特征变量数]的三位格式来组织。&lt;br /&gt;
3. 定义一个LSTM深度学习模型，通常为一个序列模型对象，逐层添加LSTM层或者其他层，最后通过一个全连接层输出到输出层。&lt;br /&gt;
4. 最后对需要的时间段进行预测。

首先对数据进行标准化，我们使用sklearn包里的MinMaxScaler函数：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
scaler = MinMaxScaler(feature_range=(0, 1))
trainstd = scaler.fit_transform(train.values.astype(float).reshape(-1, 1))
teststd = scaler.transform(test.values.astype(float).reshap
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
其次，我们将训练数据和测试数据组织成需要的格式，这个格式与我们将要建立的LSTM模型有关。这里我们对每个输入构造一个LSTM神经元，一个60个输入单元，每一个对应一个时间步。这60个单元的输出会作为一个全连接层的输入，这个全连接层直接产生下K个连续时间步的输出预测。作为防止过度拟合的正则化手段，我们在LSTM层和全连接层 之间加了一个Dropout层。这个Dropout层在训练的时候会随机放弃一部分权重的更新，但是在进行预测的时候所有权重都会被用到。&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjz5t-HhDvIvnYpVc0Z_bV82D1CQ7w6Sk6SKXPkmHNVhQUrQd6ryEGxJ2RyILLO0zZ13rBwhO5UYaGqJSk72EGCLFUqB5Xr_Op5hGyiU89bKqlPdLiO0yP_L9BCxt7pl69VnH2TmA/s1600/F15.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;275&quot; data-original-width=&quot;715&quot; height=&quot;246&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjz5t-HhDvIvnYpVc0Z_bV82D1CQ7w6Sk6SKXPkmHNVhQUrQd6ryEGxJ2RyILLO0zZ13rBwhO5UYaGqJSk72EGCLFUqB5Xr_Op5hGyiU89bKqlPdLiO0yP_L9BCxt7pl69VnH2TmA/s640/F15.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
Figure 15。LSTM网络结构 （修改自CNTK Tutorial）&lt;br /&gt;
&lt;br /&gt;
对于这样的网络结构，我们需要如下的一个函数来定义我们的数据，即将数据组织成为[批量数，时间步数，滞后特征数]的形式。这个可以通过如下的函数来实现：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
def create_dataset(dataset, timestep=1, look_back=1, look_ahead=1):
    from statsmodels.tsa.tsatools import lagmat
    import numpy as np
    ds = dataset.reshape(-1, 1)    
    dataX = lagmat(dataset, 
                   maxlag=timestep*look_back, 
                   trim=&quot;both&quot;, original=&#39;ex&#39;)
    dataY = lagmat(dataset[(timestep*look_back):], 
                  maxlag=look_ahead, 
                  trim=&quot;backward&quot;, original=&#39;ex&#39;)
    dataX = dataX.reshape(dataX.shape[0], 
                          timestep, look_back)[:-(look_ahead-1)]
    return np.array(dataX), np.array(dataY[:-(look_ahead-1)])
&lt;/code&gt;&lt;/pre&gt;
执行下面的命令就可以生成所需数据：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
lookback=1
lookahead=24
timestep=60
trainX, trainY = create_dataset(trainstd, 
                        timestep=timestep, 
                        look_back=lookback,  look_ahead=lookahead)
trainX, trainY = trainX.astype(&#39;float32&#39;), trainY.astype(&#39;float32&#39;)
truthX, truthY = create_dataset(truthstd, 
                        timestep=timestep, 
                        look_back=lookback, look_ahead=lookahead)
&lt;/code&gt;&lt;/pre&gt;
有了如图11的网络图以后，就可以开始定义我们的LSTM深度学习模型。
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
batch_size=100
model = Sequential()
model.add(LSTM(48, batch_size=batch_size, \
input\_shape=(timestep, lookback), kernel_initializer=&#39;he_uniform&#39;))
model.add(Dropout(0.15))
model.add(Dense(lookahead))
model.compile(loss=&#39;mean_squared_error&#39;, optimizer=&#39;adam&#39;)
&lt;/code&gt;&lt;/pre&gt;
调用fit方法就可以快速的训练这个模型。我们指定迭代20次，小批量数为100）：
&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
model.fit(trainX, trainY, epochs=20, batch_size=batch_size, verbose=1)
&lt;/code&gt;&lt;/pre&gt;
下图展示了拟合过程的信息：&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBYZBZ0nssFJ4JYDyU0EoZEF2FRxj0Zv47LK2S0fTEwAOrVnnSX0TYfLB5MpF8-UiMH3Suayjzs0MK68G84OpfYpcDttyhVpSJlu0YZPAEHs7GFLZ_pN5I6SIAEd4sqPCa8x1F9w/s1600/F16.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;77&quot; data-original-width=&quot;464&quot; height=&quot;106&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBYZBZ0nssFJ4JYDyU0EoZEF2FRxj0Zv47LK2S0fTEwAOrVnnSX0TYfLB5MpF8-UiMH3Suayjzs0MK68G84OpfYpcDttyhVpSJlu0YZPAEHs7GFLZ_pN5I6SIAEd4sqPCa8x1F9w/s640/F16.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&amp;nbsp;Figure 16。LSTM拟合过程信息&lt;br /&gt;
&lt;br /&gt;
那么这个模型的拟合效果如何呢？&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYXRrG-r-Gh0ehc9Q-FZ3z9Lws4yBsd1G8nQ43uhWi1QEA2I0uIh6Jny-O86yn5LC0Q7oZ8SZhbjNuW6qCpfp_4zV0xRQNndY6qpgbUFQmqfjOBeZghVn1vTXrmjDIRsQIfZJp6A/s1600/F17.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;517&quot; data-original-width=&quot;997&quot; height=&quot;330&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYXRrG-r-Gh0ehc9Q-FZ3z9Lws4yBsd1G8nQ43uhWi1QEA2I0uIh6Jny-O86yn5LC0Q7oZ8SZhbjNuW6qCpfp_4zV0xRQNndY6qpgbUFQmqfjOBeZghVn1vTXrmjDIRsQIfZJp6A/s640/F17.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&amp;nbsp;Figure 17。LSTM拟合结果&lt;br /&gt;
&lt;br /&gt;
我们看到拟合效果还不错。平均绝对误差百分比（MAPE）只有25%不到，比用传统的SARIMA模型效果要好点。其次，LSTM模型一次输出未来24个时间点的预测值，使用起来比用SARIMA迭代预测方便很多。另外需要指出的是我们也可以直接在模型中指定损失函数为MAPE，这样更好优化衡量指标。&lt;br /&gt;
&lt;br /&gt;
## 小结&lt;br /&gt;
&lt;br /&gt;
在这篇短文中，我们介绍了一个目前正在流行起来的深度学习建模环境KERAS。这个建模环境相对于传统的计算环境，比如CNTK，TensorFlow，Theano等具有抽象性高，易用性好的特点，同时又依托于这几种计算环境，具有一定的可拓展性，非常适合于从事深度学习的实践者使用。

我们看到使用KERAS可以非常直观地描述神经网络结构，几乎可以达到所见即所得的情况。我们在文中还分别介绍了三种流行的应用领域，分别是：&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;- 深度推荐模型，运用嵌入技术可以将不同类型的信息有机结合在一起构造一个深度神经网络推荐系统；&lt;br /&gt;
- 图像识别模型，运用多层卷积神经网络对图像进行切割分析，得到一个精度很好的手写数字分类器。同样的技术和模型可以直接移植到其他物体识别数据上，比如CIFAR10等。我们也介绍了运用已经训练好的现成模型进行迁移学习的手段，减少拟合的参数量，在保持一定精度的情况下提高训练速度；&lt;br /&gt;
- 简单时间序列预测模型，运用长短记忆（LSTM）神经网络模型来有效预测具备一定周期性的时间序列模型。一个非常简单的单层LSTM模型既可以达到定制的SARIMA模型的预测精度。&lt;br /&gt;
&lt;br /&gt;
如果读者对如何安装KERAS这个软件以及KERAS应用于其他领域感兴趣，可以阅读由电子工业出版社出版的[**&lt;a href=&quot;http://item.jd.com/12134119.html&quot; target=&quot;_blank&quot;&gt;《KERAS快速上手：基于Python的深度学习》&lt;/a&gt;**]</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/8884517825725074243/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/8884517825725074243' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/8884517825725074243'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/8884517825725074243'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2017/09/keras.html' title='如何零基础使用KERAS搭建实用深度模型'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgda8zT4OaoPlKQM3q8mmQKwKgKJ3V7IWGMU3nMSihHnvlsCQYGVFsBYmD296bo-vsUhqSspPbXpvEa1BFf0cW5JDH70R3aoOkgpg7YlkihE_Fem47LBjaT3R7EzOkD8V0FhtEzAQ/s72-c/F1.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-4474746609881573035</id><published>2017-06-12T18:44:00.003-07:00</published><updated>2017-09-03T01:04:45.626-07:00</updated><title type='text'>CNTK integrated into Keras</title><content type='html'>Keras is a very convenient tool to build your deep learning model from scratch, it is so easy to use that it almost becomes the de-facto deep learning modeling framework in Kaggle competition.&lt;br /&gt;
&lt;br /&gt;
Keras used to support only TensorFlow and Theano.&amp;nbsp; &lt;a href=&quot;https://www.microsoft.com/en-us/cognitive-toolkit/&quot; target=&quot;_blank&quot;&gt;CNTK&lt;/a&gt;, now rebranding as Cognitive Toolkit in full name, from Microsoft is joining as a new backend choice, but you need to download Microsoft &lt;a href=&quot;https://github.com/fchollet/keras.git&quot; target=&quot;_blank&quot;&gt;private fork of Keras&lt;/a&gt; at this moment while Microsoft is working with Keras author to fully integrate CNTK as its backend.&lt;br /&gt;
&lt;br /&gt;
CNTK is a very good deep learning tool:
1. it is &lt;a href=&quot;http://dlbench.comp.hkbu.edu.hk/&quot;&gt;super fast&lt;/a&gt; especially for RNN type of model;
2. it scales well across multiple GPUs

Here is our own speed comparsion (number in sec) on Windows platform using NVidia Titan Xp, YMMV though:

&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg7tLTCu3lcJIzaEJEsNVux7IMePM-EC3xa6ziK_R0g0xGscGPFE9q67CV58i2xgVT8GaiKJGI-hHMMQndb2nWZDjQHoeYVXVIzEWAPCVJRhG7I7-_vr60wYiE2FpXsU9v88aufKQ/s1600/DLspeed.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;251&quot; data-original-width=&quot;588&quot; height=&quot;273&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg7tLTCu3lcJIzaEJEsNVux7IMePM-EC3xa6ziK_R0g0xGscGPFE9q67CV58i2xgVT8GaiKJGI-hHMMQndb2nWZDjQHoeYVXVIzEWAPCVJRhG7I7-_vr60wYiE2FpXsU9v88aufKQ/s640/DLspeed.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
If you want to give CNTK a try, you can follow the installation instruction &lt;a href=&quot;https://docs.microsoft.com/en-us/cognitive-toolkit/Setup-CNTK-on-your-machine&quot;&gt;@ here&lt;/a&gt;.

To install Keras that supports CNTK, follow the instructions &lt;a href=&quot;https://docs.microsoft.com/en-us/cognitive-toolkit/using-cntk-with-keras&quot;&gt;@here&lt;/a&gt;.
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/4474746609881573035/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/4474746609881573035' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/4474746609881573035'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/4474746609881573035'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2017/06/cntk-integrated-into-keras.html' title='CNTK integrated into Keras'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg7tLTCu3lcJIzaEJEsNVux7IMePM-EC3xa6ziK_R0g0xGscGPFE9q67CV58i2xgVT8GaiKJGI-hHMMQndb2nWZDjQHoeYVXVIzEWAPCVJRhG7I7-_vr60wYiE2FpXsU9v88aufKQ/s72-c/DLspeed.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-5236460020952413016</id><published>2017-02-20T15:24:00.001-08:00</published><updated>2017-02-20T15:27:46.245-08:00</updated><title type='text'>Google&#39;s switch to G Suite</title><content type='html'>My paymenet information for this custom URL is expiring and Google told me to update the payment information. That&#39;s OK, I will do it. 

However, by switching to G Suite, Google makes paying this $10.00 so difficult that I decided it is not worthy to continue the business with Google. Its email contains link that won&#39;t work for me, taking me to webpages that do not answer the question. All the steps become very bumpy and it is really a bad experience for customers.

With this, now I understand why Google is &quot;good at inventing technologies but bad at making products&quot;. 

I will switch to my &lt;a href=&quot;https://programmingsas.wordpress.com/&quot;&gt;WordPress sites&lt;/a&gt; eventually. This dying out of sas-programming is also chronical with the fading out of SAS from new emerging markets (Surely SAS will still thrive in traditional analytics market for a while).
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/5236460020952413016/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/5236460020952413016' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/5236460020952413016'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/5236460020952413016'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2017/02/googles-switch-to-g-suite.html' title='Google&#39;s switch to G Suite'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-8304343186195659929</id><published>2016-08-11T14:25:00.003-07:00</published><updated>2017-01-05T20:47:28.245-08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="Theano"/><title type='text'>Install Theano under Anaconda3 Python 3.5</title><content type='html'>Update on 2017/01/05:&lt;br /&gt;
&lt;br /&gt;
With the release of Anaconda3-4.2.0 in September 2016, users are able to install mingw and libpython under conda, which makes&amp;nbsp; using Theano and keras in Python3.5 much easier.&lt;br /&gt;
Here are the simple steps:&lt;br /&gt;
1. Install Anaconda3-4.2.0. I used their &lt;a href=&quot;https://repo.continuum.io/archive/Anaconda3-4.2.0-Windows-x86_64.exe&quot;&gt;Anaconda3-4.2.0-Windows-x86_64.exe&lt;/a&gt; installer, MD5=0ca5ef4dcfe84376aad073bbb3f8db00&lt;br /&gt;
2. In your Anaconda Prompt, execute : &amp;gt;&amp;gt;conda install -c anaconda mingw libpython&lt;br /&gt;
3. Install Theano and keras. I used their github repository&lt;br /&gt;
4. Remove the following statements from __init__.py file under Theano&#39;s installation folder:&lt;br /&gt;
&lt;br /&gt;
if sys.platform == &#39;win32&#39; and sys.version\_info[0:2] == (3, 5):&lt;br /&gt;
&lt;div style=&quot;-qt-block-indent: 0; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; text-indent: 0px;&quot;&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; raise RuntimeError( &quot;Theano do not support Python 3.5 on Windows. Use Python 2.7 or 3.4.&quot;)&lt;/div&gt;
&lt;div style=&quot;-qt-block-indent: 0; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; text-indent: 0px;&quot;&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;div style=&quot;-qt-block-indent: 0; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; text-indent: 0px;&quot;&gt;
5. Input the following statement in Environment Variable under system settings:&lt;/div&gt;
&lt;div style=&quot;-qt-block-indent: 0; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; text-indent: 0px;&quot;&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;div style=&quot;-qt-block-indent: 0; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; text-indent: 0px;&quot;&gt;
THEANO_FLAG=floatX=float32,device=cpu,blas.ldflags=-LC:\\openblas -lopenblas &lt;/div&gt;
&lt;div style=&quot;-qt-block-indent: 0; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; text-indent: 0px;&quot;&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;div style=&quot;-qt-block-indent: 0; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; text-indent: 0px;&quot;&gt;
Adding GPU capability follows the general GPU installation rule as well. &lt;/div&gt;
&lt;div style=&quot;-qt-block-indent: 0; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; text-indent: 0px;&quot;&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;div style=&quot;-qt-block-indent: 0; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; text-indent: 0px;&quot;&gt;
Now you are good to go, enjoying Theano backed deep learning and&amp;nbsp; Python35&lt;/div&gt;
&lt;div style=&quot;-qt-block-indent: 0; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; text-indent: 0px;&quot;&gt;
======================= &lt;/div&gt;
As of writing, Deep Learning package Theano can&#39;t be installed under Python 3.5, which causes some problems with my project. &lt;span style=&quot;background-color: yellow;&quot;&gt;&lt;i&gt;&lt;b&gt;ruslanagit&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; provided a viable &lt;a href=&quot;https://github.com/Theano/Theano/issues/3376&quot;&gt;solution&lt;/a&gt; that works for me under Windows 10.&lt;br /&gt;
&lt;br /&gt;
For convenience, I copied his solution below:&lt;br /&gt;
&lt;br /&gt;
Well, the main problem with Python 3.5 on windows, I guess, is that mingw and libpython are not available (not complied with Python 3.5), so you cannot run $ conda install mingw libpython
 step.&lt;br /&gt;
&lt;br /&gt;
So you either need to downgrade to Python 3.4 (was not an option for me) and then follow standard instructions for installing Theano on Windows, or make a few tricks to make theano work with Python 3.5. For me the following steps worked on Windows 10 with Anaconda3 and Python 3.5:&lt;br /&gt;
&lt;br /&gt;
&lt;ul&gt;
&lt;li&gt;Install mingw from https://sourceforge.net/projects/mingw-w64/&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Add the bin directory of mingw to PATH, and make sure there is no other gcc compiler in PATH (i.e. TDM-GCC is not there).&amp;nbsp;&lt;/li&gt;
&lt;li&gt;In .theanorc file add&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&amp;nbsp;[gcc]&lt;br /&gt;
cxxflags = -shared -I&quot;[TDM-GCC path]\include&quot; -I&quot;[TDM-GCC path]\x86_64-w64-mingw32\include&quot;&lt;br /&gt;
&lt;br /&gt;
You should update paths to TDM-GCC according to your system.
 Note, that TDM include directory is required, since the compilation will fail with mingw include directories for python 3.5 (I think they would work for Python 2, but I am not 100% sure)&lt;br /&gt;
&lt;ul&gt;
&lt;li&gt;Create libpython35.a manually and copy it to appropriate directory. For example:&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;&lt;ul&gt;
&lt;li&gt;Create temp directory&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Copy python35.dll (I took it from Anaconda3 folder) into created directory&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Navigate into created directory&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Run: 
 gendef python35.dll&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Run: 
 dlltool --dllname python35.dll --def python35.def --output-lib libpython35.a&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Copy libpython35.a into Anaconda3\libs&amp;nbsp;&lt;/li&gt;
&lt;li&gt;All other installations/configurations were done as described in the guide for installaing Theano on Windows.


&lt;/li&gt;
&lt;/ul&gt;
&lt;/ol&gt;
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/8304343186195659929/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/8304343186195659929' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/8304343186195659929'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/8304343186195659929'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2016/08/install-theano-under-anaconda3-python-35.html' title='Install Theano under Anaconda3 Python 3.5'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-4173371103563186331</id><published>2016-08-10T16:13:00.002-07:00</published><updated>2016-08-10T16:52:09.451-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Jupyter Notebook"/><title type='text'>Add Table of Contents to your Jupyter Notebook</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://www.blogger.com/blogger.g?blogID=29815492&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;/a&gt;&lt;a href=&quot;https://www.blogger.com/blogger.g?blogID=29815492&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;/a&gt;&lt;/div&gt;
Jupyter notebook does not support Table Of Contents (ToC) out of box, unlike R Markdown. There are nbextensions that ppl made to support this. It is easy to add this capability, following these steps to add Table of Contents to your notebook:&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;1. Find your Jupyter Notebook  data directory by using this command in your DOS console:&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; a. Jupyter --data-dir&lt;br /&gt;
&lt;a href=&quot;https://www.blogger.com/blogger.g?blogID=29815492&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;/a&gt;&lt;a href=&quot;https://www.blogger.com/blogger.g?blogID=29815492&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;/a&gt;&lt;br /&gt;
2. Copy the following two files into your Jupyter data directory:&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp; https://rawgithub.com/minrk/ipython_extensions/master/nbextensions/toc.js&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; https://rawgithub.com/minrk/ipython_extensions/master/nbextensions/toc.css&lt;br /&gt;
&lt;br /&gt;
3. Run this command:&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; a. Jupyter nbextension enable toc --user&lt;br /&gt;
&lt;br /&gt;
4. In your Jupyter Notebook, add this command to a MarkDown cell on top of your notebook:&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; a. &amp;lt div id=&quot;toc&quot; &amp;gt &amp;lt /div &amp;gt&lt;br /&gt;
&lt;br /&gt;
One caveat is that all headings will be listed in ToC, including your Title (if there is one). One easy work around is to write your title in Word, save a screenshot into an image, save the image file to the same folder as your notebook is, then use the following command in the top Markdown cell:&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;&amp;lt div style=&quot;float: left;&quot; &amp;gt &amp;lt img src=&quot;Title.png&quot; height=&quot;200&quot; width=&quot;800&quot;&amp;gt &amp;lt /div &amp;gt</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/4173371103563186331/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/4173371103563186331' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/4173371103563186331'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/4173371103563186331'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2016/08/add-table-of-contents-to-your-jupyter.html' title='Add Table of Contents to your Jupyter Notebook'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-6433843425251797947</id><published>2015-05-10T18:09:00.001-07:00</published><updated>2015-05-10T20:59:00.950-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="PROC EXPAND"/><category scheme="http://www.blogger.com/atom/ns#" term="PROC SQL"/><title type='text'>Fast SQL moving average calculation without windowing functions</title><content type='html'>In this post, I show a trick to do moving average calculation (can be extended to other operations requiring windowing functions) that is super fast. &lt;br /&gt;
&lt;br /&gt;
Often, SAS analysts need to conduct moving average calculation and there are several options by the order of preference:&lt;br /&gt;
&lt;br /&gt;
1. PROC EXPAND
&lt;br /&gt;
2. DATA STEP
&lt;br /&gt;
3. PROC SQL
&lt;br /&gt;
&lt;br /&gt;
But many sites may not licensed SAS/ETS to use PROC EXPAND and doing moving average in DATA STEP requires some coding and is error prone. PROC SQL is a natural choice for junior programmers and in many business cases the only solution, but SAS&#39;s PROC SQL lacks windowing functions that are available&amp;nbsp;in many&amp;nbsp;DBs&amp;nbsp;to facilitate moving average calculation. One technique people usually use is CROSS JOIN, which is very expensive and not a viable solution for even medium sized data set. 

In this post, I show a trick to do moving average calculation (can be extended to other operations requiring windowing functions) that is super fast. &lt;br /&gt;
&lt;br /&gt;
Consider the simplest moving average calculation where the trailing K observations are included in the calculation, namely MA(K), here we set K=5. We first generate a 20 obs sample data, where variable ID is to be used for windowing and the variable X is to be used in MA calculation, and then we apply the standard CROSS JOIN to first examine the resulting data, Non-Grouped, just to understand how to leverage the data structure.

&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border: 1px dashed rgb(153, 153, 153); color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;&quot;&gt;&lt;code&gt;
%let nobs=20;
%let ndiff=-5;
data list;
     do id=1 to &amp;amp;nobs;
     x=id*2;
  output;
  end;
run;

options notes;
options fullstimer;
proc sql;
      create table ma as
   select a.id as aid, b.id as bid, a.id-b.id as diff, a.x as ax, b.x as bx
   from   list as a, list as b
   where a.id&amp;gt;=b.id and (a.id-b.id)&amp;lt;= abs(&amp;amp;ndiff)-1   
      having aid-bid&amp;gt;=(&amp;amp;ndiff+1)
   order by aid, bid
   ;
quit;
&lt;/code&gt;
&lt;/pre&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEij22iwNHP6W14EYXVYEEm5nwiG86yYuPxJI11QS3yRNXHwVkpmuKNZZURE-KOewcLJRsZS1F9baXbsakqRWroM1AiR21Samz9U-pWKLPpyovlXCw3JNOYbBbczosDGmKyBPoUB2w/s1600/CrossJoin.PNG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;640&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEij22iwNHP6W14EYXVYEEm5nwiG86yYuPxJI11QS3yRNXHwVkpmuKNZZURE-KOewcLJRsZS1F9baXbsakqRWroM1AiR21Samz9U-pWKLPpyovlXCw3JNOYbBbczosDGmKyBPoUB2w/s640/CrossJoin.PNG&quot; width=&quot;416&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
From the resulting data set, it is hard to find a clue, now let&#39;s sort by &quot;bid&quot; column in this data set:

&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgqzQM8-1bSF5IW7DIGhG8VR1GXfllZNq3LJ7wpQmUnyiHF2XJQTIyHZM8Jd2QpWUUDfnXpevr8tphAaP28ui8TbYLWWPBTm_5ZziDmPAXkWaHnj-H7N34ZdJim8Bgm3b9m8eA8hg/s1600/CrossJoinSorted.PNG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;400&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgqzQM8-1bSF5IW7DIGhG8VR1GXfllZNq3LJ7wpQmUnyiHF2XJQTIyHZM8Jd2QpWUUDfnXpevr8tphAaP28ui8TbYLWWPBTm_5ZziDmPAXkWaHnj-H7N34ZdJim8Bgm3b9m8eA8hg/s400/CrossJoinSorted.PNG&quot; width=&quot;335&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div align=&quot;left&quot; class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
﻿&lt;/div&gt;
From this sorted data, it is clear that we actually don&#39;t have to CROSS JOIN the whole original data set, but instead, we can generate an &quot;operation&quot; data set that contains the difference value, and let the original data set CROSS JOIN with this much smaller &quot;operation&quot; data set, and all the data we need to use for MA calculation will be there. Now let&#39;s do it: CROSS JOIN original data with &quot;operation&quot; data, sort by (a.id+ops), which is actually &quot;bid&#39; in sorted data set;


&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border: 1px dashed rgb(153, 153, 153); color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;&quot;&gt;&lt;code&gt;

%let ndiff=5;
data operation;
     do ops = 0 to &amp;amp;ndiff by 1;
     weight=1;
     output;
  end;
run;
proc sql;
      create table ma2 as
   select a.id as aid, b.ops, a.id+b.ops as id2, a.x*b.weight as ax
      from   list as a, operation as b    
   order by id2, aid
      ;
quit;
&lt;/code&gt;&lt;/pre&gt;
Note that in above code, it is necessary to have a.x multiply by b.weight so that the data can be inter-leaved, otherwise the same X value from original table will be output and MA calculation will be failed. The explicit weight variable actually adds in more flexibility to the whole MA calculation. While setting it to be 1 for all obs result in a simple MA calculation, assign different weights will help to resolve more complex MA computing, such as giving further observations less weight for a decayed MA. If different K parameter in MA(K) calculations are required, only the operation data set need to be updated which is trivial job. Now the actual code template for MA(K) calculation will be:

&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border: 1px dashed rgb(153, 153, 153); color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;&quot;&gt;&lt;code&gt;
%let ndiff=5;
data operation;
     do ops = 0 to &amp;amp;ndiff by 1;
     weight=1;
     output;
  end;
run;
proc sql noprint;
      select max(id) into :maxid
   from   list;
quit;
proc sql;
      create table ma2 as
   select a.id+b.ops as id2, avg(a.x*b.weight) as MA
      from   list as a, operation as b 
      group by id2 
   having id2&amp;gt;=&amp;amp;ndiff
     and  id2&amp;lt;=&amp;amp;maxid
   order by id2  
      ;
quit;
&lt;/code&gt;&lt;/pre&gt;
With this new method, it is interesting to compare it to the expensive self CROSS JOIN as well as to PROC EXPAND.&amp;nbsp;On my workstation (Intel i5 3.8Ghz, 32GB memory, 1TB 72K HDD), self CROSS JOIN is prohibitively long in running time (if data is large) while the new method uses only 2X as much time as PROC EXPAND, both time consumptions are trivial comparing to self CROSS JOIN. Time consumption shown below is in &quot;second&quot;.
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjv2IjMaFy3BSKqBMxFjME6eBARt7k3eqRS2GPr_d8xZvZGR3mtlFA9ofjlfW_0xVN576i-LgPJuuKwv3IWL-sOsUMLdZa81eYOtuUx5VA71Jd2rA95Y6PwNlb4S4EQc5hobRfWvw/s1600/Comparison.PNG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjv2IjMaFy3BSKqBMxFjME6eBARt7k3eqRS2GPr_d8xZvZGR3mtlFA9ofjlfW_0xVN576i-LgPJuuKwv3IWL-sOsUMLdZa81eYOtuUx5VA71Jd2rA95Y6PwNlb4S4EQc5hobRfWvw/s400/Comparison.PNG&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
Below is the code readers can run and compare yourselves.&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border: 1px dashed rgb(153, 153, 153); color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;&quot;&gt;&lt;code&gt;

%macro test(nobs, ndiff);
options nonotes;
data list;
     do id=1 to &amp;amp;nobs;
     x=id*2;
  output;
  end;
run;



%let t0 = %sysfunc(time());
options fullstimer;
proc sql;
      create table ma as
   select a.id, avg(b.x) as ma
   from   list as a, list as b
   where a.id&amp;gt;=b.id and (a.id-b.id)&amp;lt;= &amp;amp;ndiff-1
   group by a.id
   having id&amp;gt;=abs(&amp;amp;ndiff)
   ;
quit;
%let t1 = %sysfunc(time());


proc expand data=list out=ma2 method=none;
     convert x=ma / transformout=(movave 5);
run;


%let t2 = %sysfunc(time());

%let ndiff=5;
data operation;
     do ops = 0 to &amp;amp;ndiff-1 by 1;
     weight=1;
     output;
  end;
run;
proc sql noprint;
      select max(id) into :maxid
   from   list;
quit;
proc sql;
      create table ma3 as
   select a.id+b.ops as id2, avg(a.x*b.weight) as ma
   from   list as a, operation as b
   group by id2   
      having id2&amp;gt;=abs(&amp;amp;ndiff)
      ;
quit;

%let t3 = %sysfunc(time());

%let d1 = %sysfunc(round( %sysevalf(&amp;amp;t1 - &amp;amp;t0), 0.001));
%let d2 = %sysfunc(round( %sysevalf(&amp;amp;t2 - &amp;amp;t1), 0.001));
%let d3 = %sysfunc(round( %sysevalf(&amp;amp;t3 - &amp;amp;t2), 0.001));
%put SelfXJoin : &amp;amp;d1, EXPAND: &amp;amp;d2, Operation: &amp;amp;d3;
options notes;
%mend;



dm log &#39;clear&#39;;
%test(5000, -40);

%test(10000, -40);

%test(15000, -40);

%test(20000, -40);

%test(25000, -40);

%test(30000, -40);


&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/6433843425251797947/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/6433843425251797947' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/6433843425251797947'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/6433843425251797947'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2015/05/fast-sql-moving-average-calculation.html' title='Fast SQL moving average calculation without windowing functions'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEij22iwNHP6W14EYXVYEEm5nwiG86yYuPxJI11QS3yRNXHwVkpmuKNZZURE-KOewcLJRsZS1F9baXbsakqRWroM1AiR21Samz9U-pWKLPpyovlXCw3JNOYbBbczosDGmKyBPoUB2w/s72-c/CrossJoin.PNG" height="72" width="72"/><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-9077523497121553450</id><published>2015-04-25T17:33:00.002-07:00</published><updated>2015-05-10T20:59:17.801-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Data Mining"/><title type='text'>sklearn DecisionTree plot example needs pydotplus</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;div style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;
&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
In Python, &lt;a href=&quot;http://scikit-learn.org/stable/index.html&quot; target=&quot;_blank&quot;&gt;&lt;b&gt;sklearn (scikit-learn)&lt;/b&gt;&lt;/a&gt;&#39;s DecisionTree example uses pydot for plotting the generated tree:
&lt;a href=&quot;http://scikit-learn.org/stable/modules/tree.html&quot;&gt;@here&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA0AAAAKlCAIAAABt/xuQAAAgAElEQVR4nOy9faxc1XmoP0lUNaQJ+aAYAzFjnaqBpDR13BPikzqacyWkQkuLj3CrqIXIgxKa9rjEQZFw26T1CIeoMsikVEnTKDaESSHXaqnExVQx7UkPt+YKS1gViRxpik5uLGEFX8VXOuLn0/iP/fvj7Xnve9Zae8+emf09zyPLmrNnz95r1l57r2fe9dX6/1IQAQAAAEBlaCFwAAAAAPUCgQMAAACoGQgcAAAAQM1A4AAAAABqBgIHAAAAUDMQOAAAAICagcABAAAA1AwEDmB8/s//Xa3Uv7LzAwAACgKBAxif0o0NgQMAmE4QOIDxKd3YEDgAgOkkS4F7/JtPbGnPfPHBLzkbb9rx0SiKvve9729pz3zve9/P/kusc+LE81vaM855R2VLe+bEiedH/dQXH/zSroU75PWuhTu2tGc0JRkyXtrGYHHvvfIV9J9+uzh2Ldyhl/6mHR99/JtP5J/McZBSmrxP+vRn6F73/8mfzszM3P8nf5qhwE1emONY3Hvv4t57o9FvtF0Ld8gHYQw027M9ZsLN7rzlPMP9zwaLnDyZpXbQm8s+MVLyxQe/VP3CY2+fPK5XiYz6deS6+5W+yMCW9szi3nu1GIx0cFvXZ8KJE8+PJwzlkqXA7Vq4Q9zFbixL4MZmQkmSr5mTZhUpcM69NLTeReAm//fIXz2aXuA+fNNNOUXgELjKkpPAJdzszjNHbh/dkjI9Uk87G0cVuO997/u1qGIROCVO4MTjJzl45gIX1eTngUNmAqd+tqU9Yys/eb6rceu7dovexmLBYoFffPBL8ln9kWcz1/7yk6KgP/Lk+aL1yk07PmofE7aU3LTjo8FfnCdOPO+UsC8++KXkZ43ctPZL2dQ65dhm0ePffCI5PRbJFj2Fc3znF7Dz0LQVrc294In8e0lvGCc/5eJq0FHOKPsEf7LrnjZ7E/aP28dml61ggse3O8t31432QgS3Jyes4gKnmROXw/Kt7Y3pFzDNOluqgwIXvKkjUz7l7k54Sjq/f2xhO3HieVsvJhfguH0Svou+FefuWrQkizQPgxkrDwT7ET2OzXP9plJ52I32jtYQhVO20+SDfxw/o+Ju9ij0o9E+G9NUujaRImF+BC6YJw67Fu6wlyb43aV0BTPEFk57lYO3hvPwXNx7b7BU+B90bp+EohUsNrJ/8Gnj36rRxvy3945zXke4g0/Ooem58649KYufFnvZwfki9kRyU/sROFuj+Tlv3/3GN47aL6KWL7mh53KqVP8hoNtzjTFlTmYCp7kf5w02Ahf3mJYL49zV8qf9iH2C6HWKYppQHRXbsrFK08RrmmUHP4XJ11V/dcVF4PSxJYnUcrNr4Y7k9FjszaMVoXPGXQt3WKPVZNv7RA8eF45KeKbb/LR1qhOB08Pa00mV5r+O29/JwC0blcIvDAnHtxGFLakFzvmy/u391LG/n5mZ+fBNN/3r/3xxZh2VMN2osiXtpDMzM08d+3vZop/a073bETg5uHDrb/ym09jqMFTg/By2t6T9dk5/AM06G33xBS75ppbtcoSUoVw5oJU2uSgpC7CtZmT/hO/ipDD5gJKZmlfBEiI1h73lZbs9vvNMC+aevqViYb1zaD7EHcf5aiMJnC02Y0TgfIGLyxP/pPaA9vraJ0AwH6KNP5udX3H+reH/5PYPKPkZfO380vCLVlyxCRpPFH+rJgicfl85rz2+vz3uuzjpSVP8/Ad7cgTOF7iEeIdii6i9rzXUIrlh71lbBegBnd+Ti3vvHbVZv1wyEzgtOk7uBwXOiVXqPv5n7Z0T17DlVBUJ57UnslZn99EvYiuSoS1KQwVOS6d8dz27JDghPU4m28hQMFX23tAcc76gPXIwV/3nst4AznW0d4IVOGvhwTZ0m1HB/R3iBFcvetzxnWC7fTwNFbibdnzUCe85t7cGw1Sh/s//Xd3TvXtP927rcDZItqd7t7W3f/2fL1otU4F75K8etTE258/xInBODic0cASLlv8TP9r4Syl4UztlyT5Jg+jZJThtL0r6Auzcg3E1oh+rTrj19IA23+JKSNzvRj+MpI+FuPbBuAo7TT7EHceScLNHMd02HFGw/4JpSBa4uDyxOI8FJ1V63pQN9M7NHnz42J/cwWM6Ga7HcQQuWLTiik3cz9c01y6uSCccX7fHfRc/Eplc/JzbxzqiJVngHIsKPo7i+rtLO1jkVcS6/+PffMIezakdxu46XxbZCJzztYfW3xpitf+iYbnpNN7ZkHKCwEWhImKjuFtMODcyjwa90kPbT6MUAqfpkZCb1Eb6aEhIj8Vu9J/4+kHdbutCW2c7//xv5z+X7SNMH7g2MBnXB06flX6vBetJQZEK7hzFCFzc8Z1SZHdLFjinzcXPh2hd4KyHyT8bMNvTvdvG5PSt+//kT9XkdIvuaY+gDvfIXz06tsAFv2mwjc9/YtryECdwcTe1Y71DYzZ6gSTkJveL/ohKU4CTu9smfJco5v4N/jb43ve+n1BCnDxUw/D314dMsJeY7uYIXMobOe44cRniF/LkB1EmEbi4PHGO4Fwjp2OGumDys9q2oTvpibwgmfpE0EqdnLGVS7APnBathGKTkJ/BWzVB4IK7xW2P+y5OeoYWP+chHHcnJgucvUZOjaY4J9L70Wma81Pil3abQr8SqTjZCFzcszuKF7jgbZZS4FTd9NdMssBpcdFiGhdjsPtogoe2n0YpBE6PpsVIAgzaVDc0yBfFC5y9sYPBec3wlAU0+bksqbXtp1FzBS750se5lA2z2SCc3X7rb/ymo322CdU/ptPAmonACfr7wb875HnnN5IGBS54U48qcJpsiVLIzyf9EZWmAMdduKHfJRpL4IIlJEHg/IeDv788Uf3mPKcGTc6HhOM42ZJwRZJzIyuBC+ZJXCLHEzjRAi20Qx8CmsNx+TyhwAWLzdD8dG7VcgXOT15WApccS/ZPJAexoZYEgUvI4bh7pLJkIHD+RfqeGcoQFLjkQLH+GSdwzl06VOC+Z/oz6rtpbks5UZqY6lCBi9bD+3I0sTc9ckJ64tKmCXNyyUnwTetddK2VjvS49NH8tFdhqMAlN6FOLnDJTajBdvmhTahb4ru0CxqBc0TKiZ9JEM6G3yoSgbPY5j/NXltakgUu7qb2W22GVvmL612no/V+lnEtI3EE9xn6XRKObzfaEhVXQuIEzmlGCO7vlOSgwKXJh4TjWEYVuDhBSSBZ4OLyJCGRTqq2mCbUlKGBoQ8BTVjcT+uUTajBohVXbFLmpy89UXyRjts/2ngJ4ppQgwKXHKcI3imWZIFL84jwL6g8JZwb3A+tJVfo0xiBCz64tSdjUODUAPQItoFPD5IgcHqB5UGfLHDR+i9RWyxu2vFRpw+mPpGtJG2Jb5iwpBE4OZq9AWy+xaXHEidwzq9Gx+ecLYumX5FVbcvQ58hNXu/UoQIXJQ4ymFzgEo6/a+O0CMH9b9o4SEIjo/Zr+oVBBc7q1FPH/l77wNkgnA2/+RImQxZsHzhnRKpjbLf+xm/q0e7/kz+Vd51MSxY4p5htMQNRrcDpB28yY0d8gYu7qe0d8XiKQQy6m03DGAXYuZvSfJco/v5djB/EECwhcQL3+MZpOLS3mS9wus+WUBNqmnxIOI7z1dILnJP+TAQuLk8szvN8MX4QQ4LA2donjcAtmi7/Pl9MN4ghWLTiik1cfibcqnreRTNgYtEbPKHlJ7g97rvECVwUX/ycaz2GwMnF0my3T2klGOqzJcSpYW/a2K1TX9svG01nH7hgEdd70umLpjvbfgDByjhK7AO3xbS7+yXGOW/kDfKyO9ubOQr9TLflL87Q0wicUy79QHEwPZagwEWmF4u0Ovm/PJyH2qLXDcghTSTfualspZvwTNy1ccIRPysmEbi440cmb+1zJzIFKe5Hue2e6JdzVSs7YjQYG9vTvdsPqjmfeuSvHrUDTvd077bjTJ32VjvuVX3RSV6ywDnfzml92GIaprWo+D/N/VC3n/96kJs2TiMSV/wcF/Rr5aEF2NnHSUbcd4kS799dKaYRscUmKHCa/06eO/vbfWzt6PxCG5oPccdxMipZ4Jx/9nmYicDF5YnFfxTbh55uTGhCtZcpZfh/aDzGHtNJ6pb1fpxxRStYbBLyM3irRqZYWg/WGLZzyeK2x30XJz0pi9+uxGlEomECF228VeMyxP4Gi7xLL7mtKXQOogd3dG1xakehNhXfx9N0Vms8j3/ziXoV9JxIaM1M6BWX37+y82M0vvjgl4Y2hlaQ2rWzNIPFmMEE+fG9FPMPVJM4EUwp3HVkce+99mGSpgezzxgfKRcELglfU6QXZFnpqQ7aJ2nKSSlwTu83BE6oUe24ZWPrcI1S3hi+V/hKDF800yTViykUOKdsjCFwX5zmlRiah+0UBcqJjRMRTzlDW05lijhnul0ErnbYNh3srSyKrGL99rUaMVUC90UzabYyqsDVNKyOwAGMTwFBNQQOAAB8EDiA8Snd2BA4AIDpBIEDAAAAqBkIHAAAAEDNQOAAAAAAagYCBwAAAFAzEDgAAACAmoHAAQAAANQMBA4AAACgZmQvcHad2oSlptNjJ49OWKvYwVnMfij+ms3Ji6jYiZtHPRcAAADAJOQrcJkwnsCNiuOashBHgn0icAAAAFAWmQmcBq4W997rR+B2LdyxuPde2UHekpVG/YUFne36KTmmCNzj33zCLlv2+Def8Nd3U6kSFZPl0hKWt/N1TcQxeC67MOKJE8/LuTSpNjF2Tw3pxe0/xvq7AAAAMIVkI3BiSNG6ggQFzmrQTTs+qoG0xb33qsOJ1jj7BCNw1od2Ldzhh8ocgbOnCMbw4gQu7lxOBM7JAbuP/zpufwAAAIA0ZCBwTgNiQgRONp448bzdX8NOVon0rShG4OzGYFDNETg1JPtBi2NR1reC5/IFTj97046PivA551JVjdsfAAAAIA0ZCNwXH/ySbQa1PmcFTuNe2ppp/2lDpH/8oMCpBQbbTyNP4LRd0gb8LMEkyVvBcyX0gdMgn9NjT3eL2x8AAAAgDeUIXFChRhI4PXiw/TQaS+AS2jH9cyFwAAAAUBbZNKHaBsHgNCJWZZz9Fb8JVYgTuMW990owLy5VGQqcf640ApfQhIrAAQAAwNhkM4hBFSRhEIN1lJt2fFQtSnxOBCs4uCFO4OSDwfbTKGuB88+VRuASBjEgcAAAADA2xU0j4jiKThfihND86UVEfew0IvakcdaVrcAFd5Ck+r33rJA5E444aXP2ZxoRAAAASEO9l9IqcvpcpuoFAACAilBjgTtx4vm49tNanwsAAAAgmboKnMwMXExrY5HnAgAAABhKXQUOAAAAYGpB4AAAAABqBgIHAAAAUDMQOAAAAICagcABAAAA1IxsBO6nP/3puXPnfvjDH74KAAAAACNSgsD99Kc//eEPf/j666+/8cYbawAAAAAN4tVXX83pyG+88cb58+dXVlbKEbhz5869/vrrOX03AAAAgBLJT+CE119/vRyB++EPf0jsDQAAABpJ3gL3xhtvlCNweX8xAAAAgLIowHMQOAAAAIAsQeAAAAAAaobjOWfPnl1ZWQnuubKycvbsWX/7j3/841dfffUnP/lJwikQOAAAAIDM8D1HhOxHP/qRv/OPfvSjV1999cc//nHwOHHmh8ABAAAAZEmc55w9e/bVV1997bXX/LdkZhB/e5z5TanAtfZtlX+95x4p8ry//ti/XX7w2f/+7//bbvzLf/3B5QeflX9/+a8/KDI9VSbXbLn84LOzX/3u2B+/7W/vbu3beuLMCxkmqQrMzMwsLi6WnQoAgNqT7DmyioHfPPqTn/zk1Vdf/eEPf+h/xDe/aRS47Q/dtvkLHy7l1EGBE37/2Km6CNzlB5/99cf+rZhz5ZQtlx98duvD3xn74yJw3zj57QyTFORTT+5PaYoHDx6cWcdK2Nzc3PHjx1OebmZm5uDBg2Ok88iRIzMGTYDdnj4ZAAB1J8FzVlZWghE4ibQFP/KjH/2oThG4Vx689aU/2uJs/MFff+LkXZe536F//8m7Lvvx/3p66DGF1r6tt/3t3Sl3zhYEblRqlC15kFLgxJOCb40kcBNy5MiRhYUF/fP48eOaKnn98ssvF5MSAIByCXqOBN78vm4SeAv2dZPAW3CUQ3UF7sf/6+mTd13mm5ls/NH/OGw3vvRHW07eddmr/fuHHnbNE7gTZ16QttTtD93W2rdVg3O95x7RltZPPbl/bW1t+0O36T7yQrbHsfXh72gjoEibCpy2D6rM+aby2Wf/XT/+2Wf/3T+mWtTsV787+9Xv6v5ykK0Pf0eO6ZwoiCTM7iknkkPJcX7/2KnZr35Xd7M7//d//9+TpMp+UyepfrbYj//+sVMJX8o2wmqDqX5TjcDJbvqnfPG47PrGyW9rqdAIXGvfVikPEpmTt6RciYHJDrKzbHc23va3d2/+wof14L3nHrEnkn8JvzqOHDkyNzfnbFxYWJjZiPjTwsKCDYzJxsXFRfnTRuDsnvb4zpFVyxyBW1hY8I8W9xUAAJqE4zkyTCEYeJNVTf3tr732WtygB/1gRQVOEDNzdO30/u2+rr3y4K0n77rsB3/9iYSjbf7Ch22NKK4mFer2h277xslvS63Ze+4RsTfpISd18DdOflsqaa2Yb/vbu7UC9hFRCG4UX3Ga8BxTEauQP2V/Rylko+wgqiTmtPXh74isqIGJXSXEzOTUmkJN2OxXv7v14e/Ix9Ug10IROHUpu/N4qdI9E7LFJiaBhG+99eHv2PyXxPz+sVM2KxLQIiF/SvmRMvONk9/e/IUPf+rJ/Spqa+vapxtFxXTj2rrAyXbbxJ++CVWkyu/B5kfgZE95vbi4aB3L+dPuqcexluY4mSNwzqmdgwMANBjmgVtbizEzaUt95cFbN3yZ/v0n77rs9P7tyQcMRuAcFZNQit3hU0/ulwic/PmNk99OFjiNKlkd8cM/imMqNiRmI1hOsEpVyTc8a0LJAmejevJP33KCasGjOUlSmRspVTYNNnP8CFzKmKImICh8jsDZb5GmudYXuE89uV8Ebm1tzQqcxmil1KnkyUYJ5a7Fd6pLL3CCBNKS+8AlRMJ8gdM99fXBgwcROACAZBC4/wq2ORE4UTonAidKlxyBE4IC5zSGTi5wgrb3idOIlslGxyqCAuccTeJPcigRDhU43whHEri4IZm+g/pHs7FAS/pUiWnpDskCt2ZabNMMRFA1tMKXIHBpwns5CVzciUYa7vryyy/bZs3MBU6OL1hdWws1odoT0YQKANPDVAtcwX3gHIGLa0IdVeDW1m1DTEX7wPnRKcdUxCecPl62AVGOMIbAqf3onpIq38C2PvydX3/s37R5Ubc7Sic7+Ao4ksDJnk53ND9bnOP73yLYK84/iCNw2mAtew6N7Y0qcFqcEppQEwRupOGudujA2tra3Nyco00TCtzi4mLcxx2Bs0MrEoZZAAA0j6kWuMJGoQYFbm29TrXd1UcSONsGqq5gR6Fqg53tlW/VytkuG7VZ0IbxRhI4f/yEPayqmN1HzqVHsG2msoMdLnC56ZmXMlX6cVFG+VQwW4Insl/Bhgb9Vt01r7341x/7N/k6mio7esNHByXov7VEgfNnHPQHx6zFC9ya6biZMIhBhyA4owrWNk7kYQcxOEdwxiXIDkGBE0F0ZgyJm0bETm4Sl3gAgObxaiFUVOAAak3cD4O6Y8eWOs21AABQGAgcQC40VeDm5uZU4GgYBQAoCwQOIBeaKnBOEyrhNwCAUkDgAAAAAGoGAgcAAABQMxA4AAAAgJqBwAEAAADUjGkUOJlza6TZ7SvISKuCAgAAQJOYXoEbaXb78RhjHaT0pBG44HpcAAAAUHeqK3D5rcRQGAgcAAAA5EF1BS6PtVBl9Um7Otba2lpr39btD90mSxXpW7pOpbOKkT2Cbrztb+/e/IUP61u95x6xuw1dB+n3j52SJaTsYup2cSe7mqqzlpTsrMub6qfsalfOWlIAAABQd6orcIKYmaNrp/dv93XtlQdvPXnXZT/4608MPaa/Evn2h26TFSq/cfLbspClGNjmL3x4zaxEblcft6vdi8CJom1/6Db51FrqCJxdaVQXS7Wrqutr3+Q+++y/+xvV1YjAAQAANJKqC9xajJlJW+orD9664cv07z9512Wn929PPqAvcP5K5OJq4mc6pb4jZBpXi+tUN5LAiatJ5Owv//UHomWiYhKfc/ZUgdOPyNHsavEIHAAAQCOpusBJsM2JwInSORE4UbrxInCZCFzcicYTuLW1tV9/7N9mv/pdETUnFLeGwAEAAEwx1RW4PPrACaMKnO6f0ISaIHBDh7taLZv96ndVubS7mwqZHbgge6ZpQpUjAwAAQGOorsDlMQpVByXov7VEgfNHPNgjiL2txQucHC3NIAY72sC+JZam0bW1dScTdVOZs0MWnMEKDGIAAABoHtUVuHKxEbi8sRE4h9mvflfDbwAAAAACAhemCgIn7aE2/AYAAACwhsDFUbrAbX34O7R7AgAAQBAEDgAAAKBmIHAAAAAANQOBAwAAAKgZ0ytwR44cWVhYyPywCwsLR44cyfywZXHkyJGZmZnjx48Xc7qXX355ZmZG/1xcXDx48GAxpwYAAKgRCFwSc3Nzo7rLUIE7fvz43NzcSMccmxnDyy+/LBtHMlcROP3sSMzNzQUToNv9ZPiyODMz0yQhBgAAyITqClweE/laGi9wc3NzwfBVTqHHhGTYPFxYWNBULSwsLC4u6lvBeNvx48dtTA4AAADWqixwOS2ldfDgQY0JqcfYjWIbCwsLMxtJjkLZaJMInJiHIMYm7YMWTUD6E6Vnbm7OUUmbJEH8SZxSM0Glyk/Syy+/bPdU33KObLXMCpxjY9ZlncZTS8NapQEAACanugIniJk5uiYr3Du6JivcJy9mf+TIETUGDURJK6FutBqRMgLnRJV827CxpeQIXHJ47KWXXrpnI0899VTczipVjhH6p5A9JYW+SDkC52ifbNeM8r+dzUPn1PZcCV+84JAhAABA9am6wK3FmJm0pb7y4K0bvkz//pN3XXZ6//a4Q1mRUi1wWu6scKQUOKs4VuBsUCpZ4GzAL1tZ0bBfQh+4ZKf0Bc5/rZHLsQXu4MGDNm6XPnkAAABTSNUFToJtTgROlM6JwInSJUfgihS4xcVFNZLkCFwwLhhkpAicxSYmD4FbXFxUAXVyzGlCtSeyfyJwAAAA6amuwOXRB+7gwYPiLiIf2oRq/clpQk3T+0p3k85h8lrbVeVcTnexYKrkUDlNbmLN1fGhCQVOOsbFfdyRYDu0wr6mCRUAACA91RW4nEah6miD48ePqxbYAJLtMSY+N3RsgfY2k0CXP4jh4MGDNsinDab+IAYrc5PgDJhwgluaCX5vtrgjqKsFI3DOmA/Jq7hpROyXdU4XzGQGMQAAADhUV+CgLjgxRdtcOxLBDzKNCAAAgA8CB5PitzuPvXxCchc6AAAAEBC4EZgJkdW0bbXGNqGOF34TWEoLAAAgDQgcAAAAQM1A4AAAAABqBgIHAAAAUDMQOAAAAICagcBBBow6mGPU+YqHHt9O7+e8pQMsJhldkTk69aDNBydbdBrCyacG9Je4zYSE9TNGhfU2AABGYuoELqearHTKHbA5hsClr/hTroexFppzxF92Ij/SZ0Lc5HbBbMlkIYo0xX6MIpRG4NJny+LiIktuAACkpLoCl9NKDAhcHuQ3ncpIMR5f4DIMEQ0lfSaM5GTTI3Bro8g6AMCUU12By2MtVGcKN43NzMzMaHOVbrRrSdlKJbgSVBBtJtNj2jYyWTj15ZdfPnLkiMQenEZA2cHZKOtK2Vno7G5+an2cFa7W1luv9DhaJds9JQGy7KnuqTkgr+3Hndl9dYkw/yxrG5csczI2WP1rwhyzsQLnZ8sYimnnt9Ok6kn1tZOlM/Gq5HxTzdhgtvhn9L9acvrtem66s7PI21oor5KLkF16ThPsZ3VctthcdU7EurcAACmprsAJYmaOrp3ev93XtVcevPXkXZf94K8/kXzAYCjCqoBKgN3N1knqAcmLBNj1T22gQtaVt41oUqlLTaZNflaA7Gup/OS1DZmkDJ/YlGj6JTHy8WD+aP8keVeOYFcv1Qy0H7f54+RVXOTm+PHjdnuwX1TCSg/ZRuBsi56+DgqckN4R4zQlmFpnZ9F9562nnnrqno289NJL9gLpdZEXquMz5rdB+iJkf4QMTXBCtsTdjEyODQAwlKoL3FqMmUlb6isP3rrhy/TvP3nXZaf3b084Wso6IxgpceIZyYEKJ/zgVGm24rQVnibPqU1VTeJWdk9Z+9r1Rq3ABTuK2UCLFTg/VTMb16qX16oax48f92NIQa9ykuGLTnJX92wFzh5Nv3jpAmfDVzMmuJt8lmD618b6DWBLoPPLJK60O9niBPyc47N4GgBAGqoucBJscyJwonROBE6UbuwInC9wfhU70kC5hHpI6t1SBM46qFa9we9lY4ROBM5PVVDg1tajmIuLi06ag6aysLAw9CogcMECEIzAFSlwdrBIcrYE44IWBA4AIA3VFbg8+sCtbaw/FH+L07pk9wz600svvXTPPff8xV/8hW6JG1In3bRt1WUrPNtUZ2tE24QaJ3BpTGVhYcH/UkErsglYWFjwBc6mKk7gRN38g/umcvDgQf97BROWEPhML3AzKebmsFkavC5+qCmleUwicAcPHkz5K8IquMSD10L+ZJtQ0xQh3U2OL69tu6ozGYqTLTZVEuJ1jp/egwEAppnqClxOo1DXQmMLgnWGDVYFRzbYuscXuLVQZ21b5dtBDMG2J9uCqcmLE7i4IRcOweECcWEtbQVWaYj7+nECJ/vbuI79UvqWk6pkK7Jp0D1nNqIJSGir9dUhIQds/uhGX638nAniC1wwW5ymRmuTzp5x6J5+n0u/qKQsQmvruS0/RfxBDFbmgtmi94W8lZwzAAAQpPDoD6IAACAASURBVLoCNz0UWWnZaRriZiNLoPhJWBwbyAqmHKsmTCMCAJASBK58ihQ4G1xJ3xinlDKLXraVumgr9lZBipy0DwCg7iBw5ZOHwDljFbVdzGmsHPWwZU2DPFPDflF+/tfrW8QVoZxOx1JaAAAjgcABAAAA1AwEDgAAAKBmIHAAAAAANQOBy4C4qT1KoeLzoI6aV3Zui2K+1xhruqch8w5wcf0Rc+pMVqlCPiFZdUl0eq8Gb70m5RsAVAoELgMq9YweKnDlTrU1al7J0rH5pcdnqMCNN5JjqC6MOgYzzmjTCNwYJXboR4oc4JJJGZ5Q4PxZeGRLyinBAQAmpLoCl99EvpmDwKVn1LwqPqBYC4FLSCQCl5IJBS5YMoPTFgYXIwEAmJDqClxOS2k5U97rUp46v4ZdP8qfPcHuaZfx1unms3pSywLwmlpJgDNzm41O+V/KbpRKxa4t4XyFIM5KAJIGmZVNN/q5qkmye2qtlj6vdMED5wjBi+WvLnDw4EE50dzcnCQv2T/s5davYDf6+Tz0KzgLV4gu2FzVhUSdw+oKGcETBVXJHiQhW5zyPzMsOGSvghwhuBpHXLbYjyefyCmc9uaSLclleMZcX7tcmN0zeQG9mZjVRPQr2JslwSD9E61VvmMDANSR6gqcIGbm6JqscO/omqxwn7yYvQ1O2BjGjPED/1Fr68uZjSahC8zPmEUnE57UDz30kF1x/L777ktIqlOROye1qbJT3WoC5ubmrEvp65TRC3t8Gxyam5uzjutbkVZgdk9NYfq8itvHv1i26Upf60plUvUmx7ecZU8lr+xqDfZ1+lDTzEaZTq7Xh6Yw4QraEqKFPJgtsk/KCJz9hRD8iE1zcrYMjQtqVjg3qZ40OQdsStIsKbuWTuDs2sE2BxIioMG3mKMYADKn6gK3FmNm0pb6yoO3bvgy/ftP3nXZ6f3b4w5l2zIcgfMrVyf+5O+pD3T7ZM+qOdWp8GZM/EYXypQqwak4pSJ0NtraK6XAWSdwBM63LieGpAJnF0qXZI+aV0GBcy6WUzvKYXWj+lxCDRpMqt1o8zOlwMVdQSeGlCxwdk8trv6edoueN5gt/usE4pzPxtWSBc4G/JIFTrPC5lsw2LYWU4btDwObbP34qALnxBpnNoZm4zIwpT4CAExI1QVOgm1OBE6UzonAidIlR+DWTJUQrFwV2wrjROAmEbiRInDBFL788suyXWupnARubWM97Rzf2XMmFGpC4IJXMBgu9b/FWkxcMLhnkQJnu3klR+CCccE4rOolhy3XYsqwhOuk74HuZluTxxO4YGoROAAoneoKXB594DR85eDXE7bukarF2dO2SeUdgXO6vkldbusDrfMkqdqEakXBNj8Fq9L77rvvnnvu0T+PHDkSNJ64JmbbwcsRODsQLw+Bc0TBMbY0AqdthRJK1CZU609OE+rQ/u92N4kDaa8s+UZyLitwTh2vqZJDJTQg2muqP06C2eIcORktQnJZtRedZosfgbPZYlO1sLBgS93Xvva1e+6556mnntLPBsukdAP1twfLsKjbwYMH7Wq/trV9qMDZm0jLcDCjaEIFgNKprsDlMQrVaROxvdn8+lgbX6T20j2VYOtShgIXDBbqW/Ys2oIpNZbVJsGpPIL9sh2Bc76s7c3mR+A0fLK4uKihwWAH9jwEbm1je7fsP5LAaV6JAdi+g8FLoN83OaqkqRLhcAYxLCws2C5WaybD/UEMNlVBg9Ti6qi/fwmcIyd0Q9TdRNr8QQxOrvrZohno/AhxBG5tY1un/XZxwyCCZdjRRCdL5X73G0advJKk2micn6qEgaX+pVljEAMA5EB1BS4PbPBgLX4mrQSCT+c8SGhyKmbZbycgZON5KaHSyg+nJNcdZ061Wny74B3BNCIAUBjTJXC2RSY46+ZQqiBwcc062eI3Go5qYwhcroxxRSqLhJD1z2JK+IT4E/nG9Zkr7KEBAFPFdAmc03oyRv1XrsBJE1VhwQmbV2O0C6cUOGe0r98uliFOO93YX83iz0w2k910gAlkOHGuM+Xe2HfHJNhLU/3wm8BSWgBQItMlcAAAAAANAIEDAAAAqBkIHAAAAEDNQOAAAAAAagYClwH59VN25notAH++tLjO/vZbHz52cn7fY2tra7f/2ZN//OVn7W7z+x5ztsTxx19+Vg6SB/P7HjtxaqB//vmRf5nf99j8vsdu/7MndYfDx07Kdt2NHugAAFBNELgMyFvg/Clb87MKf2rW4JBAZ7sK3Cce/Adf4P78yL+kOfUff/lZ1anMsQJ34tTA8bm1dYHTLyKMN38KAABA3lRX4PJYiSEnCo7TFClwwXP5U5x8859Oi3t94sF/SKlrBWONTVNruf3PnvzmP50+fOyk8xazsAIAQAWprsDlsRbq2sY1szW4Iu2DzgxedmIqlRi7p4qOrL80dAIw33tEjyTMo4e1C3wJGoFzEm+DQ8GVhYJfX080s3Hue2cWLmfKNJvylAYpTaJOBO7Pj/zLH3/52U88+A/y1uFjJ9fW1r75T6edBk3dWbbP73vsm/90Ou5EJ04Nbv+zJ3VnPZ1E2vTfiVMDPZH+kwQkw4zEAABQNaorcIKYmaNrp/dv93XtlQdvPXnXZT/4608kHM0qlF1z2mpT3GLt/p4zZoF23SG5spfdNMolSy7K8e0C4c4a20ObUO2Smsl2ZRfq1mZQu/5Pmgicn6QE/vjLzzoCpzZ2+NjJTzz4D/qWExgT/UpzCtlTmm5t86jVvqERuARYiRwAAKpG1QVuLcbMpC31lQdv3fBl+vefvOuy0/u3xx3KNoc5Ape8ProVOH8N+/QLtIux6SpVctjkKfWHCpy/OHfCMkT2W+s88vaAQwVu1AUAfIGLG9Pge9Xtf/ZkcuxNcFRPGkOdo00icM6E+wAAAKVTdYGTYJsTgROlcyJwonTJEbg10y5p2wR9SRJNkY1OBG4SgZNQmYS+NByYicAlf2ulXgInSHtrQlsnAgcAANNGdQUujz5wBw8eDEanfEmyK1VLs6Ozp+wgrxME7p577rnvvvv0T1G3I0eOHD9+fGFhQbRgDIFzvkX6xb+twOmntC1VvlTeTaijCpz/Keldp5E5K3A6jNS2pYoC0oQKAACNoboCl8coVKe10fZm841Ee/FLW6ruqfihuLVhAmenBdG2zqDA+Q2j+pZdPV27vgUT5mMHK1jn06jkkSNHhgqcb5BBdKSCHS4QFDixLv0nHeOcAQd2/6DA+XvqsAYZWzq2wDGIAQAAqkZ1BS4PnNnLZkaf4muk4FMFsRG4sbHhySqQfrjDGDCNCAAAVJDpEriFhQUVONsGmh4ETo9TnVbFXAWu7lccAAAayXQJnNMuOUa7WF2qc2cWN0GaRzMRuLUqLTOVn8BV5zsCAABYpkvgAAAAABoAAgcAAABQMxA4AAAAgJpRjsCtrKy88cYbZX93AAAAgPrxxhtvrKyslCBwr7322vnz58v++gAAAAD14/z586+99loJAvfTn/50ZWXl9ddfJw4HAAAAkJI33njj/PnzKysr//mf/1mCwInDvfbaaysrK68CAAAAQApWVlZee+21MewtM4EDAAAAgMJA4AAAAABqBgIHAAAAUDMQOAAAAICagcABAAAA1AwEDgAAAKBmIHAAAAAANQOBAwAAAKgZCBwA5MXKysoSVJWySwcATAQCBwB5ceDAgV27dh2A6rF169aySwcATAQCBwB5ceDAgaNHj5adCggwPz9fdhIAYCIQOADICwSusiBwAHUHgQOAvEDgKgsCB1B3EDgAyAsErrIgcAB1B4EDgLwoTOC63W6r1Wq1Wt1u1/4ZRZG86PV6g8FAX0dR1Ov1dJ/x6Pf7coTl5eXgu5IYQfbsdDryZ7vdbrVa/X7fHqrdbo+dmFFB4ADqDgIHAHlRZASu3+9bAVJVUkUTnRKNGwwGzrtjIAd0zquoTco+zgdli3xwMBjIi36/L3JZAAgcQN1B4AAgLwpuQu31ehpdS1C0brerMbMEgWu32yl1yhc4UTQVOIm36Ul1/16v1+/3rbcVFoRD4ADqDgIHAHlRfB+4TqfT7XateAUFLkHvOp2ONHcOBgNtdVVsq6j9iP1zeXlZtEx31kOJw1mBEzREN0lEcCQQOIC6g8ABQF4UL3DiSXaL8+dgMIjTO+mIFuzQlkC/33c+ok2rju0tLy/LFgQOACYHgQOAvChe4Nrt9vLysg2JOUrkSFUwAqeGlyYC51tawv6OwElLqzahame4AkDgAOoOAgcAeVGwwKm39Xo9NSeraJ1OR6JlqmjBiJcOUB3aB04/7u/pR+A0VscgBgCYHAQOAPKi+GlE5E8ZNCD92PyNdv6OsZssneDcYDBwhqOqwGlMzp9VRJMhysg0IgCQHgQOAPKiChP5JitaYX3OqgYCB1B3EDgAyIuKCFywMXTyiXxrDQIHUHcQOADIiyoIHARB4ADqDgIHAHmBwFUWBA6g7iBwAJAXCFxlQeAA6g4CBwB5gcBVFgQOoO4gcACQF80QOJ0xRBfgssjSW7oYg0waYmcSlulL9LOy3oM/pYi+lXIKuglB4ADqDgIHAHnRDIFrt9uymKmz5qm8ZVVMDMx6nqNiMvRV/xQ11Nng7ItRV/QaFQQOoO4gcACQFw0QOOttnU7Hylm327VKZ5er1/2t3klwzh6h3W5bvdO3/AW7MgeBA6g7CBwA5EUBAqfrHMg6VNJ8KasaCN1uV9tAZakD2aHT6ah+2bZLZ40EXYE+MitxCXouOU6v15M/RePkpLJFjiDCp62x2tjqN5gicAAwFAQOAPKiAIFzlhPVpejFnySmpZGzbrcrbuSEyhKIEzg5o/wpL/Tg/X5f9tT9JSWaNlmqVdbaUtWzZ1R9zA8EDqDuIHAAkBcFCJyzIKk6kypdZNbLEqmKvBW0EiJwtglVOsPpdj2+2ph8St3Otr1an5PXdrV7e+QCwm8RAgdQfxA4AMiLwiJwSlDgtKuZNLNGIy6BGjeIQc8lET41Ng3aqZZJStTY/GTYb4HAAUAaEDgAyIsi+8BJTzKdkkMm7xAZ0j5wIli249pIpxAbk75u9rAarut2u9rTzn5QG16dU2vkT8NvxbSfRggcQP1B4AAgLxowCrWpIHAAdQeBA4C8QOAqCwIHUHcQOADICwSusiBwAHUHgQOAvEDgKgsCB1B3EDgAyAsErrIgcAB1B4EDgLxA4CoLAgdQdxA4AMiLmgpcp9NxppcbD5lnxB5KJxbRtbOceUYKA4EDqDsIHADkRU0Fzk4CnO2hnDnedCrgrE6XHgQOoO4gcACQFwicPZRO/Kvv2sW4Cg7CIXAAdQeBA4C8KF7gpI2y1+v1ej27SEMURbIEgiyWIMteyQtZv0H+F6NyluHStRN0T7ukvbOIqrMQliNw0frqC9HGVVZ1HdXCQOAA6g4CBwB5UbzA9Xq9wWCgPcwEaakUn+v3+6JQsmiV7NnpdMSlut2uHEGsq9friVfJEUTOnIMnEwzmyWEROACYBAQOAPKirAicri6qETIROBEmu6i8vNC+aKJ0al0SrtNhBzZKJ4wUgbOJFF3TtzQBhYHAAdQdBA4A8qLEPnCdTqff74shaQRuqMBJJMyPwFmk1TVlMoICp5LHIAYAGBsEDgDyoniB05iZuJe8ln5sGiFzXvT7fekPp33dZH/RLP2s6OBIs35ofM52oZNDOTswjQgAjAoCBwB5UZdRqMW3YJYOAgdQdxA4AMiLWgicBsbKTkihIHAAdQeBA4C8qIXATScIHEDdQeAAIC8QuMqCwAHUHQQOAPICgassCBxA3UHgACAvELjKgsAB1B0EDgDyIleB05UVJj+UTufhLzZvxzfIcAddPiGBsT+ok5vIRMTBD8o+E04dh8AB1B0EDgDyIu8IXFYrUAWn2x0MBnZuEZ0HODi7byYfjKLI7pDwwWCCRwKBA6g7CBwA5EWtBU4CXbroglqUXcM0LlXjfVDibbpOQ8IHETgAQOAAIC8mETi7SoEuP68LKtiVr0SYxHKk1VI/a9e80vnenINE8RG4KIra7bYcpNvtyv5D5WnCD3Y6HXG4hA8icACAwAFAXkwegdPIk8iNIOvKRyYC1263ZQcRuE6nY/8cSrIP6cL2KT2sgA8icACAwAFAXmQicNKlTKNlsiBpssDZxU/1g6NG4BSJh2mDpnZNG0p+H0TgAACBA4C8mFzgpIVUu4JJU2lcBE52iEwELiUJPiSni0YcizDJB6N1gWMQAwAkgMABQF5kMohBZ/fQnm0SYPvCF76gM27ozBoy6YbuqfNxJBP0IT2XbpGzaDDMxvAy+aAGDuM+2G63NSsQOIApB4EDgLyoy0S+k/tQwSBwAIDAAUBe1EjgghP5VhMm8gWACIEDgPyoi8BNIQgcQN1B4AAgLxC4yoLAAdQdBA4A8gKBqywIHEDdQeAAIC8QuMqCwAHUHQQOAPLiwIEDhw4dWoHqsWPHjrJLBwBMBAIHAHlx9OjR+VL50Ic+tH379nLTEOT666//2Mc+Vm4ayi4dADARCBwANJOzZ8/Ozs6eO3eu7IQEeO655/bs2VN2KgCgxiBwANBALl68uHPnzlOnTpWdkFgeeOCBQ4cOlZ0KAKgrCBwANJA9e/ZUf/zE7t27T5w4UXYqAKCWIHAA0DQeffTRvXv3lp2K4ayuru7cuTPNaq0AAA4IHAA0iqWlpZtvvvnSpUtlJyQVZ86c2bFjx+rqatkJAYCagcABQHOo8sCFOJ555pndu3eXnQoAqBkIHAA0hOoPXIjjwIEDDzzwQNmpAIA6gcABQEOoxcCFOHbt2vXcc8+VnQoAqA0IHAA0gcOHD9di4EIcFy5c2LZt25kzZ8pOCADUAwQOAGrPiRMnajRwIY5XXnlldnaWAQ0AkAYEDgDqzcrKyrZt2+o1cCGOY8eO3XbbbWWnAgBqAAIHADVmdXV1x44ddRy4EMfnP//5AwcOlJ0KAKg6CBwA1Jg777yzvgMXgly6dOmWW2555plnyk4IAFQaBA4A6sqhQ4dqPXAhDgY0AMBQEDgAqCXNGLgQx5kzZ7Zt23bhwoWyEwIAFQWBA4D60aSBC3E888wzt9xyS1MNFQAmBIEDgJrRvIELcRw4cODzn/982akAgCqCwAFAzWjewIUEbrvttmPHjpWdCgCoHAgcANSJQ4cO7du3r+xUFMfq6urs7Owrr7yif5abHgCoCAgcANSGEydOTGG3MB3QcPbs2fe9731LS0tlpwgAygeBA4B6sLKyMjs7e/78+bITUgLPPPPMxz72sWuvvbbVai0sLJSdHAAoHwQOAGrA6urqzp07T58+XXZCyuG+++67+uqrW61Wq9XatGnTxYsXy04RAJQMAgcANeDOO+984oknyk5FOdx+++3veMc7WutcfvnlTz/9dNmJAoCSQeAAoOocOnToc5/7XNmpKI1//ud/npmZ0Qhcq9Wam5srO1EAUDIIHABUmukcuOBw6dKle++9973vfe/P/MzPtFqtq666iuGoAFMOAgcA1cK62jQPXPBZWVmZm5u7/PLL3/rWt37lK18pOzkAUCYIHABUixdeeGHnzp2rq6vVH7hw9uzZlcL58pe//K53vesDH/hA8aeuOGUXB4BCQeAAoFrcfvvtb37zm9vt9sLCQsUHLtx44417yuD3fu/33v/+9//O7/xOKWevJps3by67OAAUCgIHABXi0qVLMtvZm9/85iuvvPJb3/pW2SlKYn5+vuwkwH/BtYBpA4EDgArxzDPPXH755Trc8j3vec/9999fdqJiQRqqA9cCpg0EDgAqRKfTUXu74oorNm3a9LWvfa3sRMWCNFQHrgVMGwgcAFSF1dVVaT+V9QZuv/32CxculJ2oJJCG6sC1gGkDgQOAqvD444//3M/93Fvf+tatW7e++OKLZSdnOHlLQ7/fF51tt9v2z+XlZQlVdjqdKIq63a6+Xl5e1n3GPm+73W61WoPBwG4cDAYaHJVzRSZiKn9KSrrdrvMR51B5gMDBtIHAAVSLUmamqAgf/OAHL7vsss9+9rP/8R//UWIyzp49m/JiFSAN4kAqQN1uV153Oh1RtOXl5X6/L2+ptOm7Y9Nutx3rkrNIknq9nrywZ+n3+6JumhI5yGAwUOHLDwQOpg0EDqBabN26tewJGcrh4x//+HXXXbd79+6yE7LnxhtvTHmxipEGibdFxtUio2iqWVaSEgSu2+2m0Slf4PTPfr8vB5d4m8hcZLxteXlZRNMG6vIOwiFwMG0gcADVYuvWrWUnoRyqs1hWehUoTBp6vZ4jXlbRpNlUGy6jkMD1ej1pzRQFlEZSRZpoLb7AKbaFVPYUh1NLE+MUjYtLT+YgcDBtIHAA1WJqBa46VFDgoihqt9vWgXwl6nQ6fnwuWm+E1ThZ+tMNFTi7c4TAARQLAgdQLRC40qmgwIkb6ViBKKREy8vLamnBCJwN4I0dgdP2U4vT9U06w9km1AQdzAoEDqYNBA6gWiBwpVM1gev1etq3TE0rqGjJgxh0gOp4feAEP/ymYxoYxABQJAgcQLWYBoGTuSf8Rj1nEoqyqI7A2XlDovX8kX5sqmjauc3m54RNlhqfi0LDYHU3P3onV1b3UWVkGhGAzEHgAKpF4wWu1+s5fd4FP35TFtURuASSFa2APmdVA4GDaQOBA6gWjRc49bZ+v2+DRs4kFKWlrz4CF2wMzWQi3zqCwMG0gcABVIvSBU6az7TXvIiCbpRmNZ1/XzrO27eE1kac7vPyQkNugjOGsYivGkMtBA4cuBYwbSBwANWiXIHr9XoyFYW0wfX7fdtrXvuqywsJ9ti30oDAQR5wLWDaQOAAqkW5Aqfd5KVTvHUs7VbV7XZ1vjEZZuj3uEqIwKmoaWc4PbWdhCLf75kIAldHuBYwbSBwANWiIhE4IShwvV5Po2g6g2v6HlcMYoA84FrAtIHAAVSLivSBa7VaDz/8sE5aoXNVyD7aB07mALNvpT+F0w4beZNQlAUC56ADI/y35FL68wAXz5RcCwAFgQOoFqULHCBwDqJu/ujg5eXlAiZ4S8mUXAsABYEDqBYIXOkgcBbrbU6kTSKmts29RKbhWgBYEDiAaoHAlU69BM6u1iBjUDqdjp32RZbYsqNJ7FtyEG0HV7Qbop2uz1lfS15XxOGqcC0AigSBA6gWCFzp1EvgJCqmc6+0223RKdu50JkLxr41lASBE4pZ6nQoVbgWAEWCwAFUCwSudGokcDLZshIZx1KlGwwGztx7/kx7CRE424Qap32ljzuJKnAtAAoGgQOoFghc6dRI4CKvX5ovcJFp5ZRpYkadKjluEIMgEz6Pl/gMqcK1ACgSBA6gWtRO4KQ9LpMqXLtqaZudEw0qhnoJnPaBa7fb0gdOlM7mpGasGJiTyelPIX9KO6wG/6rQfhpV41oAFAkCB1Ataidw0ca1GSbBOYjOD1zwNGP1EjgQuBYwbSBwANViagVOIzq6Rb2t4LUZELg6wrWAaQOBA6gWBQtcu93udDrSsqZrMOjSC9J2JpEwZxIKOweYCpz2hdcJJrrdru04ZddatXtG63NSyBmjjWMbs4rwpQSBqyNcC5g2EDiAalGkwMlCWMvLyza+pWuSynRi0brkRWbGCumXJjGzaF2wVLl0qGO/35ftI6VKOtojcDASXAuYNhA4gGpRfATO9me3q5HaLmgiYfLCjmEUpRPB0p7yTj96614JEThFRzXa2ctoQoVkuBYwbSBwANWilD5w3W5XwmkiTykFTicYcyJwDqOOUtQmVwYxQHq4FjBtIHAA1aJIgdOYmQ2YSUxO+8PpzBQSnGu32zbSJsKnsTTtA9fpdHR7SoHTz2rEToc1MI1IJkgOZ7L8vBQPPZQWCd3BWaqrAOp1LQAmB4EDqBbVH4U66jSwtaOpAhfFrIU1+aFaG2f67fV6om6dTier0w2ldtcCYEIQOIBqUX2BG3Ua2NqBwI10KLtCg4Ry1dvsOqp5U7trATAhCBxAtai+wDWeagqcnZPFNmHrfC5i1dJOqivWOxO7qHXpaJJovQFUw2aCtqHbRnaLHspf7d5ZejXnjPkvEDiYNhA4gGqBwJVOBQUuOCeLKpe4nXQZlEWupI1b7C2Kon6/rxPByDAU29YpHxk1VIbAAZQLAgdQLRC40qmgwEXenCx2/hed+ljMSV+o4anSiWDp+JKWmaXZGd+QPgJnm1AlJdqE6kT1cgWBg2kDgQOoFghc6VRT4ARxIx3zm1LgNBLmROD8g6dPCYMYAMoFgQOoFvkJnC6oMDk6t4jdqIEcjfr4cZ1kRp17QpoInS+lvb5SHsSnggJn52TRjLVBMlk2o7U+7Yu80KxorS+YoVfN7qav0+e8ntpJnrMD04gA5AcCB1At8hO4DKf/CK5t5WyRCE3c7L4+o4Zt9OvI0lvOuw0TuPEosgWzdCp+LQAyB4EDqBY1FTgNC+mfeq6UQjbq3BPqbUFHROA0IFrwNMhlUeVrAZAHCBxAtRhb4KQhTPuqdzoduy5CtC5wOmeENHuJA9lJJQQVMsX2mgoKXLTelBZt7NieciXTUYcudrtdOawd9qggcNMG1wKmDQQOoFpMEoGT2b+idZ1SVKq0zVFCXLoIvdP3fCjBJlRNgz1mhMBBIXAtYNpA4ACqxSQCZ1VMttje5XEC50wqIR8cNQJn0+AsbJ9y9v9R557QJtRg0zACN21wLWDaQOAAqsWEfeBksXkJTdmJ+6OQwMmS83GTSiSQIHB2VSUGMUBhcC1g2kDgAKrFhALX6/W0PVF7tkmXOJ0qQru3y1wPg8HATiqR5iy+wGl/O93uTA/b6/WGmpyde0I+PjQZNmrYbrf17HUXOJkWJJNDJc+rInmY5rprEcrjg8GJaUYCgYNpA4EDqBa1mMg3IQJXEeoucMGOfeOR0CSt3Q21Q2FCeuwOeXxwwq+MwMG0gcABVIu6CNyE8ZL8aMZEvsUInFrU0GZ0ueJ6f6gvhAAAIABJREFUnDw+iMABjAQCB1AtaiFwzaYAgRPLFF+R1lIdSuIsh9Uyqy/oCBXbNi04I05sa3WCwGl3w6FzBMpu7Xbb6aeY4QcROICRQOAAqgUCVzrFROBk/G/kzfmigz/EZlR0pPFRB4WknGwlykjgbPJy+iACBzASCBxAtUDgSqcYgVMVU7uywz7iBE73cZqwx4vAaYNmeh10erBl+EEEDmAkEDiAaoHAlU5hfeBkimOJw+ko3bgIXK/XsxG49GQyiEEYDAZyqDw+iMABjAQCB1AtELjSKUzgdOWxyPRsa6+jnd7snC+yUKwfZksgeWJkmeNDTiQ97YKT8PkxP/tB2SFocuk/iMABjAQCB1Atdu7cuRVKZefOnSkvVi2kIf0KaeWCwAGMBAIHADAmtZCGyedVKQAm8gUYFQQOAGBMkIbqwLWAaQOBAwAYE6ShOnAtYNpA4AAAxgRpqA5cC5g2EDgAgDFBGqoD1wKmDQQOAGBMkIbqwLWAaQOBAwAYk1znfLn22ms3b96c3/ELZtOmTVu2bMn1FGUXB4BCQeAAACrHqVOndu7ceebMmbITkhlnzpyZnZ1t0jcCKBcEDgCgWiwtLc3Ozp47d67shGTMmTNn5ufnX3zxxbITAtAEEDgAgApx7Nix+fn51dXVshOSC+fOnZudnV1aWio7IQC1B4EDAKgKhw8f3rVrV1PtTbhw4cL8/PyxY8fKTghAvUHgAAAqwf79+++8885Lly6VnZDcWV1dveWWWx599NGyEwJQYxA4AICSuXTp0ic/+cl9+/aVnZDiuHTp0u7dux944IGyEwJQVxA4AIAyWV1d3b1795e+9KWyE1I0ly5d2rt371RpK0CGIHAAAKWxuro6Pz//1a9+teyElMbnPve53bt3T0PDMUC2IHAAAOVw7ty5G2+8ke78hw8fvuWWW5o9dAMgcxA4AIASOHPmzI033vjCCy+UnZBK8PWvf31+fv78+fNlJwSgNiBwAABF88ILL2zbtu3UqVNlJ6RCHDt27MYbb2ze9MUAOYHAAQAUytLS0rZt21hUymdpaenGG28kZwDSgMABABTHsWPHtm3bRpwpjlOnTm3btu306dNlJwSg6iBwAAAF8eijjzZ4maysoHcgQBoQOACAInjggQd279598eLFshNSA2TJVMbnAiSAwAEA5M6+ffs+/elPM9tZemSGvCeeeKLshABUFAQOACBHZM2oz3/+82UnpH6srq7u2rXr8OHDZScEoIogcAAAeYGCTMilS5f27Nmzf//+shMCUDkQOACAXLhw4cL8/DwduSZn3759e/fupQEawILAAQBkw5kzZ1ZWVuS1dMNfWloqM0EN4vDhw3bJ1BMnTuBzMOUgcAAA2TA3N3fdddedO3fuzJkzO3bsYKGFbHniiSdkydTvfve7b3/727/yla+UnSKAMkHgAAAyYGlp6corr3zLW97yvve9b25ujuUE8uDpp5+en5+/4oorWq3WNddcw5wsMM0gcAAAGXD99de3Wq1Wq3XFFVd84AMfoIEvD77//e9v3rxZ8vmaa6757Gc/W3aKAEoDgQMAmJTHHntsy5YtrXU2b978a7/2a2UnqmmcO3fuuuuuaxmuvvrqCxculJ0ugHJA4AAAJuLSpUvW3q666qqrr7569+7duEW2vPTSS3Nzc9dcc83b3vY2yepNmzZ94hOfKDtdAOWAwAEATMTf/M3fvP3tb2+1Wu9+97uvu+66v/zLv0Td8uPcuXN/8Ad/cNVVV1122WWiy+fOnSs7UQAlgMABAIzPxYsXL7/88ne/+92/+qu/+swzz5SdnGnh4sWLjzzyyHXXXffWt771d3/3d8tODkAJIHAAEOZd73rXPAzj+uuvv/rqq2+66aacjl92KciGo0eP3nDDDXnkzwc/+MH3vOc9H/nIR/I4eJPYunXr0aNHyy4IkCUIHACEmW+KPeRKrqNNG3MJjh49mqs9MOZ3KAcOHEDgGgYCBwBhGmMP9aUxlyBvgYOhIHDNA4EDgDCNsYf60phLgMCVDgLXPBA4AAjTGHuoL425BAhc6SBwzQOBA4Aw+dnDYDDQWdMGg4H+2ev1er2evI6iqNvt6usoitrtdqvV6na7E55dD6j0+3050fLysmzRZMiW5eVlmxJNTL/fnzAxySBwadCL1el07J+DwcCWGdkor/WKDwaDSU7d7/f9AiknbbfbcVs6nY6UdvnTL115gMA1DwQOAMLkbQ/tdlsFqN/vy2txuCiKlpeXZUu329U6MlhfjoRIod0i1bx9ISey+8hHlpeX5ezdbld2sJV0HiBwKREH0j/F5KIoarfbomhabKym67tj4/+i0CKqhWR5edmeRUt4p9OR7U7pygkErnkgcAAQpgB70EpXqy6t3pTBYKD1cYLASRWeXB+LFDoCZyvOTqej++hJ7Q5ibOptvV4v1yAcApceLRv9fj9B0ewlSxC4TqczVKdE0XyBk5LT7XYlGRJv05Oqt/X7/V6v55eunEDgmgcCBwBhCrCH5eXlTqdj662gwCVE4LT51Wk+U7Quj9Y10Rc43SICpzEbOaZUtLKDVPlW4JzUZgsCNxLddXRLUOAS9E5b7WUfpyyp00frPwaCvygcY9MwmxO1lc/6pWuyPIgFgWseCBwAhCnGHrrdrnUgX4lsBenUl478JdPr9WyLlXMK2yfPHn8wGCBwk1PYIAbnCgYVLfiueHz6eKpG+3yB6/V63W7XKZkaSEbgICsQOAAIU4A9SHuWxL10i1Ui7QknxEXgtNpLiMBJR3LFT4w2ftnkDQYD28glH9Q6WPs55QQCNxKOXkeeEjmFJ6h3dp+4CJwOO7ADIwS/N6dzdm1ClR380pUTCFzzQOAAIEze9rC8vKw1nEqYrfakgVVeiIfF9YFTOUsTwAhWk04veMGpWRnEMDYFCFy329W+Zf4ghshcteSIlw5QTTOkwC+QEn7z39KfIgxigKxA4AAgTAHTiEhNZqtMrd50Y8vMvzD5KNTICFyn05EYW2vjpA/Sh8nqoCbGHmSkFrfxQODSYOcNiczlW15eVkXTjS0zf8fkTZZaIO0PABvldfpo6nltMvzSlQcIXPNA4AAgTCn2kNyrLBOBqxEI3IQkK1qufc6qBgLXPBA4AAhTlsDFRSOymsi3RiBwExJXZrKayLdGIHDNA4EDgDCNsYf60phLwFJapYPANQ8EDgDCNMYe6ktjLgECVzoIXPNA4AAgTGPsob405hIgcKWDwDUPBA4AwjTGHupLYy4BAlc6CFzzQOAAIEx97UFGQjiz8kbrKy44c3/YjTrZhL6rE1LIn3ETQOQ0n0h9L4FDHQVOCoM/JjpYunQyameKHMGZScQpM1IC9V1/XptMQOCaBwIHAGFqag86/W9wpXln7YROp2On19cP6lSrUitLbaqLcWnlKvNQOLP/Z0hNL4FP7QTOn25XCJYu8XgpKjLi1S4Q4v9gkBdSZnSC3263q78T8ihRCFzzQOAAIExN7UFrVl190mIFrtfr2WpS5n21R7BLZuki94PBwNbBst0/USbU9BL41E7g1NvsWqVRTOnqdrt2OTjdqPZvg7hyWF2GRO3QFiEEDtKAwAFAmJzsQVeOj6KobZBWJKnqdDY4WSzSviUHsRPrO42eWnEGa0ErcO12W45jRU0bv5waWtdBso1fkrb8FtRC4OLQCyexLv1TX0cbl2SQIJlsUWdylsd1FlGVF87c0X7p0hVOfYFT45dE6g5SzPRPKUX2LAgcpAGBA4AwediDLvgoTVS2panX64kwWXNSn0sf4kovcFqJdjodu06lLoLkC5x+UOtmR+myBYELolExuS79fl8vtCyPZvfRRa6cheqTSSlw2gDqCJwUDHtAZ6lTKfl2By2EEQIH6UDgACBMHvZgVziVViqtqOwqqFrVSQ3tr6+VEIHTRi7trmRxBM6eWlu1NAFWC2z1LDtHptkrp4UsEbggGqCVwJUtMHqlnFCrNHw7np0QgdMmVKfsOaVLw8ka6pPdgqbobNQQnaDFL0LgIB0IHACEyTUCJwQFTkImuga5BDMSFkj1T5FyEIO+lrBKv9+XD+oRnEEMivU2HdaQx6JMCFwQp19aUOBsB0dt60wfKB1pEEO0UfFtGmwinTZW59eF/iqIEDhIBwIHAGHy7gPX7XZ1qUqRNo1haIhFqj0nvJHyFE47rD21Hkr+1IpWQzLaQmr3t32qZH/bVy+r/LEgcHHotfjMZz6jRcVZSFf3GQwGYyx+aqeMsW39TunSc9khC37ptQNfbEr8IqSlLtuRMQhc80DgACBMY+yhvjTmEtRuFGrzQOCaBwIHAGEaYw/1pTGXAIErHQSueSBwABCmMfZQXxpzCRC40kHgmgcCBwBhGmMP9aUxlwCBKx0ErnkgcAAQpjH2UF8acwkQuNJB4JoHAgcAYRpjD/WlMZcAgSsdBK55IHAAEKZ29mDXFM/kUK2NSyz4yyXlTe0uQRx1FDhZaS2rozmz0EkBS78yxOQgcM0DgQOAMHW0h6wcy5/xNUM7TE8dL0GQOgpccCWPsQ9lfwzocm1FgsA1DwQOAMLU0R4yEbjgEvXdbpcI3NhMucDJMqy6BtdI8wlnBQLXPBA4AAhTsD1Ii5I0KtnFJWXie21y0hdSEepE9rr0lrPoQrQ+tX2v17MtVgnrYOoU+bpRPojAjU3BAieFRFa70hIii2JJ4ZFFGmTFKnkhBUZ2dlZU06URZLEE3VMlTJdbUJzlU+1KrHr2gjUOgWseCBwAhCnSHmQp0n6/b6s0XY9SeyNptyFRK22K0mVMxbF0xVU5gqxoNMZqV6KJkrYIgZuAggVOr77dqEVFfhhI4ZGVduX6ttttNXUpM1qopFjKEYIHj0OOH5k+cJ1OR7Y4vyjyBoFrHggcAIQpPgJn+wnZlSh1qXjtOSQv1PAGg4EVOB2CYOMuTg+2hAicIrW4BvP8g+QNAjceWgD8P/vrK99r4bG/EzT4agXOFhUtD7Z1NSECp/E/Ld76MyDDJto0IHDNA4EDgDCl2IP6loQuUgqc1spOBM4/+EiJcQ5CBG5sSukDJw2Xtl0+SidwWoqcCJxFAsbpE6MROE2ANO9m8D3TgcA1DwQOAMIUaQ8aI7EBM4l86P/Sc6jT6cgWaYrS2EZkoh2R6QPX7XZ1e8oWK/2so2sI3NgULHDayUxsSYuTxtJs8dBrbd+N1kPC0oNNi5nzOn2S7DQiWpLz+O5xIHDNA4EDgDDVtweNnTSV6l+ClNRiFKpG4BoJAtc8EDgACFNxe9CORwVHxYqk4pcgPdUXOAnFBbtCNgMErnkgcAAQpjH2UF8acwmqL3CNB4FrHggcAIRpjD3Ul8ZcAgSudBC45oHAAUCYxthDfWnMJUDgSgeBax4IHACEaYw91JfGXAIErnQQuOaBwAFAmPzsIf2MHkNJnpFhpFEOOsmcs3RSGnQaFDs32OSzRSBwQ9EFFSY/VPKwGLnE6Uc56JpvaXaWRb30T50Gxe7gF7CRQOCaBwIHAGHyswedy3RyEuZm07fSVLoyCFFe63QS6eeViKtWJ5xtH4FLQ1bTfyRcLBWskWbfTV/OZXZDPVdckbaTyY0KAtc8EDgACFN3gbNL0ScH4XT1LeeDKeeZk8hNUBMROKHuAme9baQgXJpyLos62OIaF/1F4MCCwAFAmLHtQRqAdDr7brerTUJ25StxJqkatd1K9rR1ZMJCk1G8wOnqqFGKas8utSQv5JgpBU4X0PSrdgROGFvg9OrL0mqtVuv555931kIQgZOSI3/KhdDP+u3aFnuuuIulC95HppwMJaXAdbtd24SqX8ovewgcWBA4AAgziT1InRSt10aCVpBasWmFpIvQS6WVvqKaXOBkraTIVMzaoW3ULkd+qA+BEyaJwMmSpvpat+tldZbKVRmS/53uZQmUInCyQzCR/hYEDiwIHACEmcQeVMU0iiANQ8kCZ9c21ZpvvAhcZOq/5DVMdQXMYLfxUb81AhdkwiZU7X+mAmf1Ok7g7GXVD44XgdPynF4Ho3QCZ1PiHNn/LAIHFgQOAMJMaA/SEKntQSI3cQLXbrdtBC49WQ1iiLzISmv0Rbr8GheBEyYUOGnLlqIiQh/FR+BkWfpolM5qQomDGHwvlCZjZzcEDiwIHACEmdAepOubvJYol/z/mc98RsMn2lgpb0UmQJKymkwQOJ15QXaw/cSDtMwgBhuzGfpBCS5a4dPXCJwwocDJpdQstWXmjjvu0NiVdqCUCK6N3aY5S/LFkvCwlgT5TZJwNC3bGopOOLgVOCcaZz+IwIEFgQOAMLWwh+Tm0dJB4IRaTOQ74cUqAAQOLAgcAISphT1MPllufjCRr1IXgXMiqdWBiXzBB4EDgDCNsYf60phLUAuBazYIXPNA4AAgTGPsob405hIgcKWDwDUPBA4AwjTGHupLYy4BAlc6CFzzQOAAIExj7KG+NOYSIHClg8A1DwQOAAJcvHixmiMDpgoEDrICgWseCBwAbGB1dfXw4cPbtm372Z/92a115r3vfe91111XdiomokkC9/M///NlZ+dEXH311WUn4f9x7bXXvve97x3pI+9617sQuIaBwAHAf3H+/PkDBw7Mzs4ePXr04sWLZSdnUg4cOLC0tFR2KqAhbN26tewk/D9eeeWVnTt3Pvroo2UnBMoEgQOA6OzZs/v27duxY8eTTz556dKlspOTDQgcZEilBC6KokuXLh04cGB+fv7MmTNlpwXKAYEDmGoGg8GePXvm5+effvrpstOSMQgcZEjVBE44ffr0jh07Dh061JjfXZAeBA5gSjl9+vSuXbtuvvnmploOAgcZUk2Bi9ZDcTt37iQUN20gcABTx9LS0vz8/K5du06fPl12WnIEgYMMqazACYTiphAEDmCKePrpp2dnZ/fs2TMNP9YROMiQigtctB6K27FjR7N/mIGCwAE0n0uXLh09enR2dnbfvn0rKytlJ6cgEDjIkOoLnHD69OnZ2dkDBw4Qims8CBxAk7l48eLhw4flgX7u3Lmyk1MoCBxkSF0ELiIUNzUgcADN5MKFC4cOHdq2bdvhw4cvXLhQdnJKAIGDDKmRwAlnzpzZuXMnobgGg8ABNI1z587JfLxf//rXGzAf79ggcJAhtRO4KIouXbp06NAhQnFNBYEDaA46H++xY8f42Y3AQYbUUeCEM2fOzM/PHzhwYJp/zjUSBA6gCch8vDfffPNzzz1XdlqqAgIHGVJfgRMeffTR2dnZF198seyEQGYgcAD1Rubj3bVr16lTp8pOS7VA4CBD6i5wURStrKzcfPPNn/vc5wjFNQMEDqCuLC0t3XLLLXfeeScdXIIgcJAhDRA44dFHH92xYwehuAaAwAHUj6effnrnzp2f/OQnB4NB2WmpLggcZEhjBC6KorNnz+7atYtQXN1B4ABqg52Pd9omdRsDBA4ypEkCJxw9epRQXK1B4ABqgJ2P9/z582Unpx4gcJAhzRO4iFBczUHgACrNhQsXHnjggW3bth06dGg65+MdGwQOMqSRAicQiqspCBxARTl37tz+/ftnZ2cPHz7M7+MxQOAgQxoscBGhuHqCwAFUjsFgIPPxHj16lPl4R+XUqVO//Mu/fMMNN1x33XW/8Au/cMMNN2zfvn11dbXsdEEtuffee2+44YYbbrhh06ZN8qLX65WdqLwgFFcvEDiACnHmzJk9e/bMz88//fTTZaelrly6dOmqq65qGa6//vqyEwV15aGHHtq0aZOWpauvvvpb3/pW2YnKEUJxNQKBAyia1dVVPyAk8/HedttttPpNzm/91m+96U1vkhr3iiuuOHjwYNkpgrqyurpqfw9s2rRpGsyGUFwtQOAACmV1dfX973///v37dcvS0tLNN9/88Y9/nPl4s+KFF1649tprNWTCuF2YhLm5ORW4//bf/lvZySkICcXt27eP7geVBYEDKI7V1dVf+qVfestb3vLe97734sWLMh/v3r17mY83c66++mqpcW+88cay0wL15u/+7u+uueaaVqt1zTXXHD9+vOzkFMqTTz45OztLs0A1QeAACmJ1dfWGG2648sorW63W5s2bP/KRj+zfv5/5eHPi7rvvftvb3rZp06ZHHnmk7LRAvbl48aJ0g7vqqqumcFDR+fPnd+/evXfvXkJxVQOBAygCa2/Cli1bprAyKIxXXnnlyiuv3LRpE7UOTM7CwkKr1VpYWCg7IaVx7NgxQnFVA4EDyB3f3qRv1qFDh8pOWpOZmZn56Ec/WnYqoAksLS296U1veuGFF8pOSJkQiqsaCFzDOXbs2LZt2+ahPD70oQ9de+21b37zmy+//PKrrrpq06ZNV1999czMzK/8yq888MADZReQkbnhhhvKztG0bN269QMf+EDZqUjLxz/+8bKvbdFIUKfsjE/LO97xjrKTkJZt27YdO3Ys16v2zDPP6JaVlZVTp07ldDpIAIFrOEePHj1w4EDZqZhqPv3pT//2b//2E088sbS0tLKyUvc5CObn58tOQlrOnj1bo9yuUcZmxdGjR48ePVp2KtJy5syZspOQlgMHDuSasRcuXNizZ8+ePXtkcb/t27dfe+21LPRXPAhcw0HgSqdetdRQptAzimEKM7Zht0Z1yFvghGeeeWbHjh2f+tSn3vnOd77lLW/ZsWNH3mcEBwSu4SBwpdOwWmoKPaMYpjBjG3ZrVIdiBC6Kou9///vvfve7pVPvtdde+0d/9EcFnBQUBK7hIHCl07Baago9oximMGMbdmtUh8IE7kMf+tDb3vY2OzDrH//xHws4LwgIXMPJVeD6/b7ct+122/65vLzc6XRarVan04miqNvt6uvl5WXdZ+zzttvtVqsVnPxWji/niqJIktFqteRPSUm325U/B4OBvJvrPLoNq6Uy8QzNecl8/bPX6/V6Pb1kWqLkU3Ld9fKNgRSAfr/vvyUHl5IcRZEmQwqqlltn/+ChxgOBGxu9WHLj65+DwcCWGdku+2jRmuTedwqJIifVp1DklS55LvV6PfnTL10TUozALS0tvf3tb3/nO99pB9dv3ryZuS0LA4FrOHlH4KTq1Ydgt9uV151OR2s+qeS63a4+5vTdsWm32/6TVx7Kun0wGNiz9Pt9eY5rSuQgg8HAPmozB4GLo91uqwD1+315LQ4nG+V69ft93aIXcWy63a5vXcvLy05xcvaRynV5eVmLkOygVfLkIHCTIA6kf+odrQ8K+VUZRVGv19OLG3yMpCeo71o29IVTurSEdzod2e6UrskpLAJ38eLFpaWlP/zDP/zFX/zFq6666u1vf/ub3/zm7du3M8NlMSBwDaeAJlR9MqqrRUbR9MllJSlB4Lrdbhqd8p+8opL2sBJusTagTimiaQN1+QXhELgEbHBUXmj15v8qiBIFTqrwodcxKHASEbE2aSMotmYVY7OBuqyCcAjchGjZ6Pf7+hzQB4VeKXvjJwhcp9NJ1imnkNgPOsrolC7dQX6Z+KVrctIL3OHDh3fs2JHJ3CUf+9jHPvjBD27evPmtb31ru93O5JjTwLZt21588cXxLjQC13CK6QPX6/Uc8bKKJjWrfRr6AqeNERrYsGF5/7nmP3l7vZ42mFp3bLfbzu9debDa5+bkEcEEELgE5FrY62sjcFEUOVffFzhtfnWazxTnygYFTgMhWsdH5reEDQFKwbMCZ1M7CdlmbC3I/NborqNb9EGhv9/s5fMfI/KrT4uNU5bsI84pJEqn05EioQLnlC49uxRmv3RNng/pBe7AgQN5LK7AlCLpmeQSIHANp7BBDO1229aUvhJ1Oh0/PhetV8Cj1oLBJ6/+/nYeqfLEROAyIXPP6Ha79ur7SiQ/D+S1I3CO/KU8XVzMzG9MlzKDwOVEHreGE4VNL3DyO3OMeKoTv7f9O21Z1dI1DQIH6UHgIJZiBE4eYU4fFEeJlpeX9TkVjMD5fX5HisBpxWwf0PpWZJ7g8txM2ZIyOQhcAtKwZctDUIn0SsVF4PTyjReBs+86yZOelLpdCrkWsOSjjQQCNzmOXkehJlQNjEWJvwOFhAicIoUkmBi/7EVG+KSo+6VrchC4GoHAQSwFCJw8g6IoWl5etsOsfEVLHsSgQ7HG6wNnu+I5P3xFCBjEkAkZeoZ1epUwX+DsbnF94NT4x+sDpydy3nJqVgYxZEu2t4b2lbQx+DEGMegA1ZRDCoK7aUOqoqWrUoMYELjSQeAglmKmEREf0u4jNqaiERH7OJuwyVJr62hjAEYSoHWqH72TTnL6lFRlZBqR9GTiGXbekGhjlanVW1DoJxyF6vRwklrW6UUXmdln7OAJLXKCFvWxE+OAwI2NnTckMpdPflLa0TAtb3aPse99p5DoMFhnqhq/dEXrTzD7s8QpXROCwFkke/3fWpnP3jIeCBzEUtZEvsmKlmufs6qBwI1Ecq+yyacRqSwIXB4kK1qufSdKBIFTtEnd9jUUMg98jgcCB7GUKHDBxtBMJvKtFwjcSNiJfB0mn8i3yiBweRBXZjKZyLeyIHCKM0ZEt+cxe8t4IHAQS1kCBwoCB2mYwoxt2K1RHWokcLb5W3/261IW0hhtu1hIk7QztZ52jVDsqCbdzf4szGPw73ggcBALAlc6DaulptAzimEKM7Zht0Z1qIvA6QAy7RqhjTP9ft/O9qIvxOfSx8wQOASuxiBwpdOwWmoKPaMYpjBjG3ZrVIe6CJz2qNFBBupYqnR22hfpIOt3hE2IwKmoOfPL5DF7y3ggcBALAlc6DaulptAzimEKM7Zht0Z1qIvA+VM4+QJn10iUGaBGGsnEIAYErsbUUeDk51Rcv2N/kl6NkEt3Cn8yCP1Bpl0r7P7OiArZmNX0+lHjaqlaeIZTEhycH9zOuNfgRNB2uhAbEhAyGVVdi4zNlrrcGgnjrvxB007pcgrPGKVrDOoicJF5CHc6He0Dp3OvSM5oHzg76U/6GXzkFE47bJTD7C3jgcBBLLUTOH+6XSH4Q8q8QJDNAAAgAElEQVSufuNM1CkT6MtuzuSr+kJ/gekTs9vtZj4iqS61VEqq7xnBKVsV+Xmgf/oruTnLc/krLLXbbWdqukxGVVc/YzOnFreG2rn/ZPALj1O6/MIzaukajxoJHCBwEEvtBE69zYlsB0eDy7o3Or+5LqXlrE7orGCjFbxE5vREUhNn/o1qUUulp/qe4ZQE+5a0v9irbKcOjqLI7yJty1hk1v6yOxCBG49a3Br2aeMvjWULT7B02cIzRukaDwSuRiBwEEvmAqfdRaMo0uHcdtT3YDCwSzLIT0x5Sx9/CUud+pol+IOJJJxu42r+YtVRFEkaIrO4jR5Znra2yVVbXTPMsVrUUumZ3DNsEWobbDmxC3gEi5BtH3cuWVxJiNZV3vazkXNpHSzzFNi16m0Zk2XZnJBtRBPquGRya8jVl2vkP5H0p5oWEl2PQa+y7UqvpU7esr8BnLbOYOGJTOlyCk80eukaGwSuRiBwEEu2AqfBKl2GWZ9Wugi07qOLXDkL1SeTUuB0hdOhAqdhPO080VrvYKeioE0keij6wMUxoWc4RciuhKblxNaa6nPpi1BcSdBFx22hjUw3Ji238qlgGYs2rror2xG48Zj81nDWNbZL0bc29nyPTAHTO30oCQLnFB6ndMUVnvSlaxIQuBqBwEEs2Qqc9vqUn7BOKMJZqjlar9j8fr4JETh92jpDjZzR4Bqh0VCfGp7zBPQHQ7TbbY3JReaRqrMBZbteEwJncYqQE5DQpVE1/6Vc+UUoIQIXVxKcUmdTIkEXp0w6Wx5++GH7Z6fTcSYvmNDhELgxsMVAio0/6Zfe1/6dLiRE4CLThGo13S88TunyH1Cjlq5JsgWBqxEIHMSSUwRO//QFzvYCkWefX/smMNIgBnveYNd1jZEo2pAqsR/Z2Fpvk9Uf0xkuUo7AWZwiFBQ4qVB1YXKJVaQvQsmDGCJGoVaGDCNw+qcvcKLakSl7I03cmjCIIWIUKkwMAgex5NcHTl9INSmv5bGoby0vL4+x+Kn8qvabQpzR4JoeZ0GVuPCbvOs0gujvY9mSx2qbCJyDLUKa4U450fiEdqMcqQg5JcH5LAJXETLsA9dqtb7whS/oU8hZi1njXtF62Ug/2Fy7XvgrBEQIXBlkMlBXsTevPI4y7D+TBgQOYqndKNTmgcBBGqYwYxt2a1SHZgtcJo4r2DmAtBdjwWvbI3AQCwJXOg2rpabQM4phCjO2YbdGdUDgUmLnANKOkgUvzIDAQSwIXOk0rJaaQs8ohinM2IbdGtWhsgLXW1/nxk4VpEsviE5JI7i0ZmpruOzsDNTV1m0dz9taX61BTueMHfG70EQbm1DlLBl2gE4DAgexIHCl07Baago9oximMGMbdmtUh8oKnM69YjdKr0FtzRSLkqFLumSOfEQmEFCB0zmnWusz/PkHjyNhDiDtzF0MCBzEgsCVTsNqqSn0jGKYwoxt2K1RHSorcM5ANLsaqc4/YGcDkBctM3GjFTg7dYsMMXYGsSVE4BLmAJIAYd5ZoSBwEAsCVzoNq6Wm0DOKYQoztmG3RnWorMAJEmbTWZfTC5zM/+JH4CyyXln6xPgRuAyHuKYBgYNY6ihwznrPE2JH6eexUtZQGlZL1cIzuhvXFM/kaHn3jKlFxmZLLW4Nf8X6sfFDRDlRWYHTmJkGzCQgp9v9aarsNMvqc9rXTd9yXqdPkgqc1g5Fht8iBA4SqKPA+VMljY3c/P5Mv0XO9FOLWio9dfGMrARO19PMm7pkbIbU5dbI5Imhna6cyYfzoLICNx4F/+QuGAQOYplygZMovQqcrgxR5DijutRSKamLZ2Ty0JclkiY/ThrqkrEZUpdbIxOB07DQNEfgxsCZk7l5IHAQS8ECJ51GxZC0A6lUgS2zfKQEw+yYI9u51QqchtOjjUPQ5d3kRQwlxu4v1VDwRNt1qaVSkrdnyDWVKtNe32iUIhRt7DejFYBfhGzLiy1s+lk5SwEah8DlgfTH0Akj9CkhTyf5X3pW6VWWFj37rLCr9NpWNike/nIvFn/R1QKmGWuSwDUeBA5iKVjgggO5teepPDF1uLiuaqqtVN1uV6xLF73RB6XOr5hSv2QUerSxD5w8iAsOyCNwIyEFwLnK0k8lfRGKzPq2drKAkYpQZNbg6nQ69IHLnAJujW63OxgMgmtVaUdJ7XQrneJF5nQCC7sUr+wmR5DtUixHSlIBZQmBqxEIHMRSSgRODUkjKHbokI420hfajVTqYxU4/cUsz1N/LdSECJzOEqkfl1CNPW8xIHAjYctMtHElyvRFKDJTQ9my4Reh5AicluQC+k0icHngLKxshxHoTzvbjX15edm2AKioOV3mpWD4S2cmR+AEfRDlBwJXIxA4iKWUPnASLev1ejpbT0qBk0eqH4GzjPr408d0v9+XBGS4EksaELjxkECFqnxKgdPL7UTgLOmLkB6tgI7nCFx+SKis3W7LD7n0AqelyEbgHCTIN2piJvtCQ0DgagQCB7EULHAaM9OAWWvYKHHxKnltp2fUKYI0AGOHoKdPkm1C1SMUOdE2AjcSWjC0o5IUIRshG1qEbMdn/azo4KhFSBOT2zf+LxC4zLExs2i9JNj/g8XJfkqET8uMLZC6PWX/SH0eFlCWqiBwuqDC5IeylyDuROl/k+v1TZM2a/NO+5JztLGbxRE4iKUWo1D9qRSbBAJXAA0oQtXM2Fyp5q2R4Sj4sqiCwEXrbS+THyehzcRODpVSoSRVssrq0J3tpKQa9fcV3IYJRgWBg1iqL3D6w7TshORFNWupsamgZzSjCFUwY/OmmreGbQGoKdMjcOptKYXM7hZcyMEi46V0bgTZGGwxR+AgF6ovcI2nmrXU2EyhZxTDFGZsw26N6lCAwNnBRvIL6vnnn9emZ1EcETjpQhOtTwNkP2ulxw5Zc36PJQicdk5NGTfVoehRirC9uJo9bFyLOQIHuYDAlU7Daqkp9IximMKMbditUR2KicDp1E6RCVBFxma0sdJOvxKtjwVJ31RdisDJbv5hZRVXf2cEDrIHgSudhtVSU+gZxTCFGduwW6M6FNaEKnIjUwfIFhtaixM4G2azC1SMEYHTJtSUcwvYJtTkFl6bEsfh/BMhcJALCFzpNKyWmkLPKIYpzNiG3RrVoTCBkwlWVKFa65MvBgVOJ3IadYxIiYMY/AhccBIiBA5yIT+B09nwJz9U8khs7TCRMlUa37ZzT6T5rJzIv6snHPzfsFoqD8/QOfEnR4f6+0sYDT2FlsNRZ4lzPj7eoxyBy4oMhyAEF+KU4jr0FFIU06REio2zp5xlvPGwhQmclHmpBey8Kq1W64477miZ1cl0u79E3lCSo2t2cmY5V/IN6EwVLutlJ3xBDR/Kp/REOtFphMBBTuQagctq7obk7gtyk9u+Cwnoc1Am5JSN6Z/mcU+KSSbiR+DSkKHA+Zc7ZRHSXi+tFHNE6bpe/kEQuJTkdGtkuNSK/4jTg6eZ2zlNSnQH//fw2BOaVGQUalYUPPX6GCBwkAt1Fzhb6Q59lsldpNWwHjy9eyFwQ6mdwI1UhIQ0dbP8cEfgJqGOAqdlI81PypFS4jcCInBC8kS+5cJEvpAjYwucXYNSW0s17m1XnpFWhiiKdLh4sCkqYZXAhOeUnTUxuc+ppNN/Ysoihim/NQI3lDE8w85gLsXAb0NxipA2Vehn7bM7ob+zXwDSFyFl6OJIUqgQuAkZ79awy5vKCy0PdrFjKTm6JIw+slqp11OOQgKnk4elCQuNKnBE4KYQBA5imTACp08oW5+pzejTTWpQuwi97J8yrJKJwDmLYzrbU4LADWU8z7A9f+1FjCtCzowD6dtQJhe4oSFb3QGBm5Cxbw01bKeno7NUrrPaqZaE9NeoSIHz90TgpgEEDmKZXOBkHXp9imlH1Ci+9rWjB/SD40XgbDtFgg46P6Pt7EQjiRcCN5SxPUMusdU4W0KCAmdXpbRXf6QIXMoipAytcXXhB8FRAQQuPWPfGqpiWpxsT/Y4gbMXTq/yqBE4bUJNI2fpBS64GwI3DSBwEMuEAqeNEZHp3B0XgZP15qMUS5Q4ZDiIwXliylrU6VOCwA1lbM+QgiRlSReoifsNMPaMA5MMYrDpSXO5icBNyCS3hqi/zvgvd3qcwMna88FVLJMpYBCDTaeTPARuGkDgIJbJBzFojEHDIRJgu/vuuzX4oT1L5KetDZykMbnk55TtQSXnSngKO09M+zr5g5H5La6tsZoqBE4Z2zMke6U86DWVMmMna3BmHLB7pjlLsMp0ilDcxAE2cixzkyYXYFvvTj6hAAI3EtL1TV7bMvOZz3xGr6BTzCITu015OwfHadlOeAmFRM+uwcLgj0MbTpY9dSIMBG4aQOAgllpM5Dv2c6owEDil4p6R4QjE8UDg0lP9WyOrgfbjUYzA3XLLLXugPLZt24bAQZi6CFywO1FFGOknu0/1a6mRqLhnBCfyLQYm8h2V6t8a/3979xMax3n+AXxwbTe2seMmke3aStcNcQglARUWurVV70JFUIsbJKLWDjGJtumhFLdxqQjGrYOWljpUtIGobQ6mVWm2iKKDDDq4IIPKCuqDKW7xQYct6KCDDjrosIc97GF+hyd6fq9ndmffmZ2Z99/3czDSer37ep6deb/7zjvvdF3INx/5LOS7u7u7Caq12+0EVfYR4KxnRICzm/69VCwO5ox8OLhhLds19CEf4MBoCHCWQ4BTzrJeysGckQ8HN6xlu4Y+EOAcgQBnOQQ45SzrpRzMGflwcMNatmvoAwHOEQhwlkOAU86yXsrBnJEPBzesZbuGPhDgHIEAZzkEOOUs66UczBn5cHDDWrZr6AMBzhEIcJZbWFgYGRmJdVXzW2+99d3vfjejS6YT0KoxCYyOjtp0MD116lRum+7y5cu5vVeEfJrhZoAbHR3NYdumQpMD0eTkZN/njIyM2HTMgV4Q4CzXarViXc+8uLj46quv1uv1jK6XTmB4eFh1EwbVarVUfxBSk9tGW1xc/M53vpPb20V47bXX/vKXv2T9Ltvb26prm7e4Rye1NDkQvf322xMTE//973+jn2bTMQd6QYCDz7RarWvXrk1NTe3s7KhuyxPOnj2rugmQt1arVSwWNfko7uzsjIyMbG1tqW4IqKTPgWhlZaVYLOIOCoAAB77v+2tra8VicWlpSXVDutDnuAm5uXbtmlafxvX19Uql0ul0VDcElNHqQLSzszMxMTEzM4PPpMsQ4Fyn7cAb0+q4CTm4d+/elStXVLci6Pbt2zdu3FDdClBGwwPR/Pz86Ojoo0ePVDcE1ECAc5rOA29Mw+MmZGd3d7dUKu3u7qpuSBfj4+P37t1T3QpQQ88D0cbGxujo6NzcnOqGgAIIcI7Sf+CN6XnchIxMT0+vrKyobkV3mAznMm0PRJ1O5xe/+MXY2Bg+ma5BgHOREQNvTNvjJqRuZWVlenpadSuiYDKcszQ/ED148GBkZGRxcVF1QyA/CHBu2d3dnZ6evnLliv4Db0zz4yakZWdnp1gs6r/8we3bt7E4toP0PxC1Wq0f/OAHU1NTes5AgNQhwDlkZWWlVCppe36qF/2Pm5CKqakpU1ZGuHTp0urqqupWQK5MORAtLy+PjIzg8+kCBDgn0MDb9PS0id/MTDluwiCWlpauXbumuhWyaLDQwaV3XWbQgWhnZ+fSpUvXr19vt9uq2wIZQoCz39LSUrFYNG7gjRl03IRktra2SqWS/idPRevr6+Pj45gM5w7jDkR37twpFotYZMRiCHA229nZmZqaMnTgjRl33IS4JiYmHjx4oLoVsWEynFNMPBBtbm6Ojo7evn0b3zSshABnraWlpZGREXMH3piJx02Qt7CwMDMzo7oVCWEynDsMPRB1Op3bt29XKpXNzU3VbYGUIcBZaGtra2JiwvSBN2bocRNk0MlTc2fqYDKcO4w+ED169KhUKt25c0d1QyBNCHC2WVhYeOWVVywYeGNGHzch2tjYmOlzdLAynCNMPxC12+2ZmZmJiQmD1pCCaAhw9tja2rp06dIPf/hDOwbemOnHTehlfn7ejjlkuE2qC+w4ENEq7jZ9w3cZApwlFhYWRkZGTFlGKxY7jpsQ0Gw2R0dHrRm4wm1SrWfNgWh3d/fKlSvXrl0z67pvCEOAMx4NvFm8N1pz3ATW6XQqlcrjx49VNyQ1uE2q9Sw7EC0uLpZKpYcPH6puCCSHAGc2iwfemGXHTfB9f25ubm5uTnUrUobJcHaz70C0tbU1NjaGRUbMhQBnKrrUdGZmxtaBN2bfcdNxjx49sjXo8GS4Vqv1zjvvWPl/dJatB6K5ubmxsTEsMmIiBDgjLSwslEolE9c+TcDW46abOp3O6Oioxb3F+Pj4H/7wh+Hh4cOHD2OJOJtYfCB69OjR6OjowsKC6oZAPAhwhuGBN3OXzorL4uOmg2ZnZ+fn51W3IkM/+9nPhoaGPM/zPO/b3/626uZAauw+ENEiI1NTU1hkxCAIcCb55JNP3Bl4Y3YfN53y8OHD8fFx1a3ISqvVunDhwvDwsLfn1KlTqhsFqXHhQLS2tlYqlXA9tSkQ4Mywubk5NjY2OzvrzsAbc+G46YJ2u10qlSy+TrPVan3zm998/vnnOcB98YtfXF9fV90uSIcjByJeZMTBvsY4CHAGmJ+fHx0dNX3B+sQcOW5ab2ZmxoVJNn/961+Hh4efffZZz/P27dv3ve99T3WLIB1OHYhokRFnOx1TIMBpjQfeXL6czanjpq3W1tampqZUtyInu7u7r7/++jPPPEODcKqbA+lw7UBEi4z88pe/dLn30RwCnL4cH3i7e/fu9evXr1+/fvr0afrhj3/8o+pGQQxra2u/+c1vfN9vtVrFYtG1ydF3794dHh4+dOiQs7uwHf785z8HDkR///vfVTcqP3Nzc5VKxeLLxo2GAKeFZrO5vb3Nv25sbGDg7ZNPPvnCF77A04kOHjxYrVZVNwpiePfddw8fPvzVr371nXfeWVpaUt2coHv37lUyNjo6+uyzzxYKhazfSDc2zYL/8Y9/fOTIET4QHTt27MMPP1TdqFzRIiN37tzhR7a3t+1ePd4UCHDqtdvtc+fOjY2N+b7f6XToG8/GxobqdinWarVOnjzJx80zZ85gJMMsZ8+e9TzvqaeeGhoa+tWvfqW6OUELCwtzc3Ob2fvnP/+Zw7voY25uzqbJjltbW6dOnRIvTHFtLNn3/U6nIy4ycv78+ZMnTzq4HXSDAKfem2+++fnPf/6555773e9+V6lU5ubmXB54E1UqFT5uujYBxXTb29tit3f06NFvfOMbWl3XtrCwYFPO0Id9G/bll1/mT/Irr7yiujnK0CIj77333rFjxz73uc9dvHhRdYtchwCn2Mcff3z69Gk6NDzzzDP/+te/VLdII4uLi7Rxnn766Z/+9KeqmwMx/OlPf+L1bA8cOHDmzJmPP/5YdaOeYF/O0IR9G/bDDz987rnnPM87ceLE73//e9XNUenf//43nxgZHh7++c9/rrpFTkOAU6nZbHInRwHuwoULqhulkXa7feLECVoQ1eL1w6z02muv8bnvF198sdlsqm5RkH05QxP2bdidnR0aTj516pT1956O0Ol0XnrppX379nGfdfLkycePH6tul7sQ4JShqW/ek5566qnl5WXVTdPI5OSk53lf+cpXVDcE4jlz5gx9J/n+97+v55QA+3KGJqzcsOfPn/c87/z586obotLy8vLx48ePHz8u9lkvvviiVlMjnIIAp8ybb7558OBB6uSGhoa+9rWv/frXv8Y8/YDV1dX9+/djARGzbGxsHDly5PTp03fv3lXdlp7Syhm1Wo16snK5LP7abDYLhYLneXT1ND1Oz2k0GvScRqMx+FvXajXxQXpTeiPxkUKhQL+Wy2XxX3FjBmmJyMoA97e//W3//v1OLSDSVbvdXl1dfffdd7/0pS8NDQ0dOHDg4MGDb7/9tup2OSq1APfo0SO1F66b5eWXX963b9+xY8deeOGFYrGY1stKHjdzWEAhLeVy+eDBgxcuXFDdEFmSCygYVIIEzp07d+TIka9//etK3l2yBCnmDMpA/CuHp0KhQOeOG40Gx7t6vc5PGyTAVatVzmTig/T6/EOj0RDPX9dqNYpu5XKZHqeWNxqNtJbpkd+wBu0FFy9ePHDgwMWLF1U3RJb8XpD4LUql0rlz544dO7Zv375XX301xcbbIYfhmNQC3Nra2uzsbNbNtUYWU4IWFhYkSyD/TB2srq6qboIs+a7LylEK9vDhQ1WnTVWVoF6vUwCq1+scyzjAcW5rNpsc7yICXLlcjo5T9Xq965gZxzKOjDTeJqZGekK9Xq/VamJuC8fBZGzdCwxa3G52dlZyw87Ozg6+olu73cZMuIBUNmxfCHD2sDXAGcTWrssgCktQ3cOPcICrVquU1ei8Kv1tOMBVq1Xx1Gpggqx4VrRcLlMyC8S4crlMA2wc4HiYjTIcvzslTopxgdYOCHuBcjkHOAhDgIN4EOCUQ9elnNoS0NQ3/lU+wNFJWB4n66tQKPCpUnEOXLPZ5MAnRkke+UOAcwECnHIIcBAPApxy6LqUU1gCCkDiicjwKVQeGPN7jMCJqStiBI5fmU/dhhsTfnFfOIVKk+HEU6hpXceAvUA5BDjlEOAgHgQ45dB1KaeqBNVqlRPVIBcx0OS2wPhZGF+LIL4g4xOprNFo0NO0uogBe0FGEOCUQ4DTTq/r7fm0hdrVSp0NcPrUxdmuy+USiOuG+HsXDdA8NvGkJM1vC8xjG+QqVHGNEtrOzWaT3oUjHW//8KoinPA4MiZuSYCzewFtyfC1IKkv1NKXswFOqxIgwOml11dVOkyL15cp4WyA06cuznZdKEFX0bPKBgxw2tKqBLnhs+fitEKS+hhnX24GON1KgACnkV7X2wfWBVA4COdmgNOqLm52XShBL+IgmSithXz1pFUJchO4HIQfz2Khlr7cDHC6lQABLgY+JtKsXjprwIuS0ykDfg5fkEW4unw2hPERttflWuKHQ+1Xaj0DHJ+goQ0lUxfxr+hFTKmLnl1X1xLwBqddJjD1CiWAxPQsgXimmz7bgQNRYC+gs8+BVfR4nRfGn3ZxMqJ4qi6L63z70jPAuVYCBLgYKFnzICpfZi8uki5ewF+r1XjiiMzra9VLdaVngKNtzpO4ZeoSWNc+mlZ10bPr6lWCer3OW178gZaiRQkgGQ1LwIPBfNEufwOJ2AvEv+pLt/SgW4BzsAQIcDEEInn4+i/xCn/6DAWu+fcjhxl6XW8vnifK7cPRlYYBTlyVijaaTF3EpRaIKXXRs+vqVQI+koobnFeXQAkgGQ1LwGP83t4Md/6gRuwF4SVaIoZ/OCUEupUsFmrpS8MA52AJEOBiCESxcFDwheXI6ZMRDnDRPG1manelYYDzE9UlnB6i6VMXDbsuv3cJ+OBIIY8CGQVolAAS07AE4U9gOD2E94Jea+z1egutZtDrFuAcLAECXAxiwKeQzjHf25vKw8+hKvaaYtxL4Hp7XnWTXxbLiITxRqO5hjJ1Ef8q1lvQr+LdhHKui4Zdl9+jBDwyx6dTxc2OEuiDNmDXE9Dh7i0wwMCrvpHwV1a+5z2JG9y70rMEPITMdyGjC2t67QWBv5J/i8BJQD+0d+RAwwDnu1cCBDiIQc8A5xQ9uy6nWFYCnjvY9VxB4PsnpXP+lfo/McDRnHH+NXz/rkKhYGuAc4qeAc4pCHAQDwKccui6lLOsBJy3+G6qjAbPOMDR+SYxwNFtUsUFewNTwukcAgc4msCAAGcBBDjlEOAgHgQ45dB1KZdzCfjUP4UnOjHEKyPwsjh8+kZcSYdeIXCViTioJk4bCp/rpEDGT+bpB/QrzSUVA1zgsi06RcUv22w2u168kgD2AuUQ4JRDgIN4EOCUQ9elXJ4l4FExmlMo3oree3Iutu/7tVotsIxLXxEBjt6IAxylMV8IcPw4BTieJM7LLfHj9LLhywATw16gHAKccghwEA8CnHLoupTLswS8HrK3t354eBkqvuZXXKJSvKQjYgTOf3KJRHHdlsDzacxPbIz4a2AJmEKhEHjkt7/9rfjrgBkOe4FyCHDKIcBBPAhwyqHrUk7JCBz/Gg5w5XKZB7f44nf5a3KjL2LAVajQFQKccghwmQtckzUgPlaKX6nzXBnOvgDnxVnkJQKNN8gvipGYfV1XuiVIcXfrJecS8J5+69Yt+kG8TZD4HPq/iyvpyOCDSXjNeh8BLhfhi3kTo+rLL4qRmGUBLsUS8PzUwV8qGgJc5uIu5Bv9UvwJ4/2TJ5rkw74AF2sVx2iBniwjlnVdfkol4B1BvNdNRuwrgXHsK0GKR49AYs6IZQHOT6kEPFVUfh5qYghwmUsxwIlX7It39sjz1qgIcBEQ4JJJK8DRDzl88bWvBMaxrwQIcMqlewAP36ohdQhwUWgaL9+3kU9k+L5PA6Q0WE1nBOgHPo/GpzDEACeOrPIzOX5FTzQOX7FPcrtrB9EqwNH256UKeKNx4WiD+3snmHzfLxQKfKsAHssM3OuCjn107lvcvBF36uQnuBbgepWAZrjTn3xLBp5fL1kCWvFVvgT0LghwLtCqBOKxgi86oZFg+b2Ajx58dQhPTAzsBRF36uQnuBbgEpTAE+7WwHdZTKsE/t6aiJn+rxHgolC3FOiSacIvLy9OlaYFk+iZvDITDZhxgOMzO/QKfAW+ZGMCV+wHHs+NbgHOf3IbilubdipeOJ6CHe3AVAh6AqeHQOCmFa1i7YFuBji/Rwl4y/M0ULqnjWQJeM2wuAPMtN+l89/rQasSuEmrEoSPFTzPT34v4KMHPY1eIdle4GCAS1AC6sS5U/CFA/jgJfBz6ZoR4KIE7rEoTvXlDwf3PfwDX9JFkS7Qn/E3g/CE64gRuPAV+/x4nudPff0CnDjiIm5S3hXFNajq9bo4IEo7Jxcu8HWKvplhBC5aRAl4y/MeQT/Il4AGUPEklXAAABOCSURBVMVN2rcEvu9Tas/0f61VCdykVQkCxwpxp5DfC+joEegF/L3zNuJegBG4sAQlEC+mEXuNVErAM+EyhQAnpVwu85VZgdpHBLhAYug6t5rOisq3JBARch5+8zULcITGQcvlcmC8s2+Ao50zMPwTIJ7j7svBAEe6lkCm65IpAX23jtuYwf5DfWhYAtdoWAI6VtDUdXH4R3IvCAz/BMTaCxwMcCRWCfgJ4V5jwBLw62AO3BNyDnCcsvlUurd3BxsiTvqhH6gbo595SXT+ZsD/luJgryGECAhwIvHbT1m4vxBv23CBxLmMlOf4Z/GvxC9hko3hwSGn0kOvEkTsI+K2jSgB/yx5DRDvoTnsFFqVYHApLqAQPrGQEX1KEDhWiIegiANRYC/gTzvfbZb+edy9wBe6raxPzugT4JKVIHC0SasE4uBc1sciBLj0iauZ20erAJdAihcFq6JP15UMSqChVAaPsZKLPAv2An0CXDKprEeoFgJcyngYRnVDsmJ6gBMHRA1leteFEmgorQBHP+BC4L4s2AtMD3DUU+cw6SU7CHAQj+kBzgKmd10W0KoEqazkIr+AQvR1JFjJxR2mBzgLIMBBPAhwyqHrUk6rEqSykkvqCyhgJRfrIcAphwAH8SDAKYeuSzmtSpDKSi7yCyhgJRcgCHDKIcBBPAhwyqHrUk7DEgy4kkuKa1j4WMnFDQhwyiHAfYbvrDD4S4nLIvR6I8mrHAIrDYrfjGXQPwk0Y8CZm8oDHE3KSeWlaOgi0GPRgzLjB72GH7q+S6AKg1zsorzrSnHyNa08EihoRiUIvIvRJRDxYSHxSi5pLaDg1EoumuwFtM1lWkIf+K7vkux6WOUBLusS0IN9+0rem/p+yem6dwxYAgS4z6S1+GH09eHUYfCttKJxWKEvzdxOmWbwfyfcmEGuOFMe4FK89jtccV5Rlu67FfFveUhDZsfr9dEyND2kcu95El5zJ/USUCjp9YKGlmBAFqxhobwEmuwFki3hw2Z4hZfEHwblAS7TEnAvSXMMIv4tXSFEX58insZNDX/nHKQECHCfySHAibmtb8FoT+OlovnFJbMXv354j0WAI+GK8+Gyb8LmzStzhEWA6yV83Ey9BNFrWxhaggFZsIaF8hJoshdItoR7gXDOQIDzu5WAcxvf5bwrcXv2jXok3QztRIATb2PKt5/nkwjina9oPJNPRoj/VnImb0QxxI8CxfaIBtP1X+HzPpKTUcQAF/j86RzgxLNdvISBOPJM24TKx3fI4Ik+nvTtZf1uuYr3wOiYKO60MrnfrAAn3lWQfgiUgHaTVEoQcdxMsQTU1K7Hej1LAH3lvxfwLXZ4vRVxrRbqEXiiYWAvELubcG+SeC8gMjmGE2G4e9I2wA1SAvqKIm66uCXgbRK9ecWELbmGf/j2jAhwUricYgYK3zSTo5W3dwW++GtfqQQ4MayEH5dhaIDzhc+3uH14q4rnAsTC8baVH0lFgOuFJ613LQEf0QYvQT4Bjl85/DRtSwDRVO0F4btgB64UyXMvILYGOD9pCXiISzJRdX1mdgEu/GoIcFJorxCXNRIvku8V4MQZwfwPk43AicWO6DkCgxZi7pQPXuIJJrNOofJW4kaKKyP0CnBiRXgjxx2B48Nc32OiuAqDfadQuRPqWoJeAS5BCSJOHqVbAt/36cgeeFDbEkA0JXuBeH4gIj2EP+pxh3/k9wLJ53CmCSdCnQNcshJUhbuR8j9MPAga3V2K3ySjB2W4/V1fBAGuPx5x9ff6FXEFy0CAoyf40ie2WYoXMQR2NrpeTLIZRl/EQFfA8fkIcWWEcICjkZXomQpd4SKGCPSdhD75gRKEA1ziEoSPmxmVwO925sLXuwQQIf+9oFAo0BG4V3qgo1bOe4EvF+DMvYghQQnC/8e+wiVI/SIGsZ2BTwgCnHRT9ibr8MAADbDdunWLfuXb0fBJdHEIQSbJRReDr7SnX6lr7PXkQIALjB713Wnpjej/W6/XI06qyssnwNVqNXF8hcvBX62oMxYryNMWPelFUrrmKnHihd/tiiEizqr0n9y84XcRn0nHIH5xmXYG5NN10YwT+lkswRtvvMGfq8FL0HXwLFkJarVa1wMoj4iIsxeMKEEET481LHjb9j0w0ruE+0jxYBhL/nsBfcjpz/fee4/3grqw8Ap/5093L+DhhvC/5XfnkapeMaL65GI6vBdoHuCSlYDPmyU+i+0/eYIuogR8GOQRu64l4E8FvybvEQhwGtH/En39A1w+5CepZETnrisf8pNUMmJoCTK9/k5y+IdndESEhug3IoaWIEVq9wLNA1w+zC0BAlzKohfyVS7WV8MwywKcl3QFxQHpP/aQj67DP/kwugT6rGFBZHYiBLheFO4FgxwDLQtwhpYAAQ5isCnAGcqmrstQWZegLLeSiw5rWPA/kXkOApxNbApwhkKAg3gQ4JRD16VcDiWQXMlF+RoWvvTilAhwlkGAUw4BDuJBgFMOXZdyOZRAciUX5WtYyK9thABnGQQ45RDgIB4EOOXQdSmXTwk8iZVc1K5hwe1pNpt9B/wQ4CyDAKeceQGuUqnMgjoTExOz0gEOxcrCxMSEfNdF9YJ0xSpB4vQgs5KLwjUsAgN7fuTlqNwqcUEv+qscAhz2gixUKhX5ADc9Pa26vRaqVComBbjd3d01UG1zc1OmWNvb26pbaq3t7W2UQC3JEug//GP9Si7YC7IjuRdsbm6qbqm1dnd3E+w+saQW4AAADGJEgLN7JRcAGAQCHAC4CDkjI9iwAPlAgAMAFyFnZAQbFiAfCHAA4CLkjIxgwwLkAwEOAFyEnJERbFiAfCDAAYCLcljD4oMPPrh582ambxHXzZs3P/jgg0zfQn4lFwAYBAIcALgohzUsJicnb968mfW7xHLz5s3Jycms30VyDQsAGAQCHABA+j799NOrV6+qbkUXV69e/fTTT1W3AgAGhQAHAJCyx48fl0qlVquluiFdtFqtUqn0+PFj1Q0BgIEgwAEApKnVao2MjITvcKWPZrM5MjKiZ74EAEkIcAAAaZqamlpeXlbdij6Wl5enpqZUtwIAkkOAAwBIzdzc3I0bN1S3QsqNGzfm5uZUtwIAEkKAAwBIx9ra2vj4eKfTUd0QKZ1OZ3x8fG1tTXVDACAJBDgAgBRsb28Xi0WzVtAwsc0AQBDgAAAG1el0KpXKgwcPVDcktgcPHlQqFVNGDQGAIcABAAxqZmZmfn5edSsSmp+fn5mZUd0KAIgHAQ4AYCBLS0t6rtkr7+rVq0tLS6pbAQAxIMABACS3sbGh7Zq98mh1342NDdUNAQBZCHAAAAlR7tF5zV55zWbTgiQK4A4EOACAhK5evar/mr3ylpeXr1y5oroVACAFAQ4AIIn5+XlT1uyVd+PGjY8++kh1KwCgPwQ4AIDYHj58ODY2Zt/qG51OZ2xsbH19XXVDAKAPBDgAgHh2dnYsXv92e3t7ZGTE1v8dgDUQ4AAAYqA7UNk9RrW+vo7VfQE0hwAHABDD7OysC7PEPvroo+vXr6tuBQD0hAAHABCl0+msrKzQz/fu3XPnOs0rV64sLi7Sz/fu3VPbGAAIQIADAIiyvr5+6NChH/3oR//73/+cWimt1WoVi8X//Oc/P/nJTw4dOrS6uqq6RQDw/xDgAACivPXWW57nHTly5Mtf/vLa2prq5uRqfX397NmzR44c8TxvcnJSdXMA4P8hwAEARDlz5ozneZ7nHT169NSpU//4xz9Utygna2trp0+fPnbsGP33T548icsaAPSBAAcA0NP6+vrx48e9PQcOHDh79uzu7q7qdmWu3W4///zz+/fv5//78ePHcRYVQB8IcAAAPU1OTnKCGR4evnz5crvdVt2onLTb7cuXL/MApOd53/rWt1Q3CgA+gwAHANATxZdDhw6dPXv2wYMHqpujwMOHD8+dO3fo0CGcRQXQCgIcAEB36+vrhw8fHhoaev/9910OLp1O5/333z9x4sThw4dxFhVAEwhwAJDEzMzMtO1eeumlo0ePvv7663m+6czMjJ4lmJycfPrpp1944YU831QJ+RIAKIQABwBJlEqlNdvNzs7ev38/5zctlUraluD+/fu3bt3K+U3zJ18CAIUQ4AAgiUqloroJdpLfsChBRrBhwQgIcACQBDq5jCDAKYcNC0ZAgAOAJNDJZQQBTjlsWDACAhwAJIFOLiMIcMphw4IREOAAIAnLOrlCoeB5Xr1eDzxerVY9z6tWq7m1xNkA16sE5XLZ87xarZZbSyzbsGArBDgASMKmTq5arVJuKBQK4uP1ep2iW7VabTQa+TTGzQDXqwS1Wo2iW7lcbjab+TTGpg0LFkOAA4AkbOrkODTUajVxBIhzW6PRyG0Qzs0A16sEnNvq9Xpug3A2bViwGAIcACSRZydHJ9fK5XKj0aAfPM/jB6nvp189z2s2m7VajZ/GL+I9qVwui69PP/B4D+H00Gg0xOdnSs8AN3gJms1moARiJu5VAn6cR0NzgAAHRkCAA4AkcuvkeEiG0kOtVuMsVSgU6K/4OfV6nbr8crksP2CDABdNYQkQ4AB6QYADgCRy6+ToMgJS20N/VSgUKGBRsKAHachHfIQf7zsCxzOx+Fd6ET3Tg1klkByBC5SAM3Qg2GUKAQ6MgAAHAEnkP/zDv4bTQ7VapUDWbDbph3CAi4CLGKIpLAEuYgDoBQEOAJLIfwKW53m3bt2iH+iqApqGFXiO7/s0AUucANcXPZ8yBE3zosdpFheWEVFYAnpZLCMCEIAABwBJoJPLiJ4BzinYsGAEBDgASAKdXEYQ4JTDhgUjIMABQBLo5DKCAKccNiwYAQEOAJJAJ5cRBDjlsGHBCAhwAJAEOrmMIMAphw0LRkCAA4Ak0MllBAFOOWxYMAICHAAkYUQnR+tcDP46tKoFL3KRKcsCXFoloHWAA6vEZcSIDQuAAAcASZjSyaWSHmgdOHFxsuxYFuD8lErg+36z2USAA2AIcACQhCmdXLqRK4f7ASDA9YIAByBCgAOAJLLu5OiUWa1Wq1ar4m00qRcvFAq01j/d7Yp+qNfr9E/EewAE7qlACYCfKZ4SjbhZKsvhlvb6BDixBHwSmbZnshLQPRVoGyYoAQIcgAgBDgCSyLqTq9frjUYjcAMlumMmBwtKFfRMvmkp9fEUO/y99FCv1+ml6BXor+LenanRaDg1B65rCegOpwlKwFmNXiFBCRDgAEQIcACQRD7DP3TPTXqELyPgjrzRaNAgDf/A90TniBC4NSfd2JSGfwKn9vqOwOVzR1R9AlygBOKvCUpAo3SEwlzcEiDAAYgQ4AAgidw6ORqw4RAgGeBqtRqN7gRG4ETNZlM+kxUKBZr9lvVd1fUJcKxcLtfrddrmPAIXtwSBs6UkVgkQ4ABECHAAkETWnRyPmYkTsGgSFT8e+IHO2fEz/b15bxQs6Gd6Gv8seUpUHBbiEcGM6BPgxBLwgBmVgP6MWwIuX71ej1sCHv/TahoigEIIcACQhJ6dHA//mEufAJeMUyUAUAgBDgCS0LCT41GirFf6yJTRAc61EgAohAAHAEmgk8uI0QHODtiwYAQEOABIAp1cRhDglMOGBSMgwAFAEujkMoIApxw2LBgBAQ4AkkAnlxEEOOWwYcEICHAAkEQWnRytW5HK/HdeeURc9YOXouj7Frw6Rt836rq2CK+pkaDlagOcPiWg5Uj6loDeJbzkb6w1SgIQ4MAICHAAkERGnRyvlzsgXlc2/OLNZjN6LTEODX1XfaNVbemVA3/Fy9jGpXwETocS8O3OaBXABG/kD7CgCQIcGAEBDgCSMC49iKGhXC7LvEvXmweIOLeFcwYC3CAl4O3J9+OK9Ub8IghwYDEEOABIIkEnx+fUms0mnaq7f/8+3+GAunNKD7RMP/3Kt2wKnxTj1fyZ+F6BTl2MAjxyFo1u1t7rb8U4Es4Kega4cAkWFxcDW49KwFueT1DmWQKOd73CWeBlEeDAQQhwAJBEsk5OvPelOADDfS0P/1AU4Ntf0p/yd8NMJcBFj/2YGOD83iUQTwfTWc7A3U7zLAECHEBfCHAAkETiTo66f7onPT0ijuv0CnDiGA//w1jDP2LekjlLKHMFA0eZcBzRNsD5oRLwlP/oAJdnCfgUKk+Gi4AAB25CgAOAJBJ3cpRsqGels3J+7xG4RqMhjsDJG2QGvdie6BBm6EUMYgnq9Trfdb5rgKMbz/v5lgAXMQD0hQAHAEkk7uRovIenqNOwTaFQ8DzvjTfeoJ/9vaEdepx68fAYT4Sunbo4A8z3/Wq12nV0RxxVohDm9bgcNTB2xWHI1zvAiSXgu5fypuYS0CP0J00HzK0E/l4V+l4RzC9IzxRLgAAHdkOAA4AkNO/kZOZOZUrnAJcP5SVAgAO7IcABQBKad3JdV5HNjTh6F5dNAU5VCbCQL7gAAQ4AkkAnlxFrApy5sGHBCAhwAJAEOrmMIMAphw0LRkCAA4Ak0MllBAFOOWxYMAICHAAkgU4uIwhwymHDghEQ4AAgiWKxuAAZKBaLKIFa8iUAUAgBDgCSWFpaUt3P2mlpaQklUEu+BAAKIcABAAAAGAYBDgAAAMAwCHAAAAAAhkGAAwAAADAMAhwAAACAYRDgAAAAAAyDAAcAAABgGAQ4AAAAAMMgwAEAAAAYBgEOAAAAwDAIcAAAAACG+T8FV6mseNviEgAAAABJRU5ErkJggg==&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; height=&quot;325&quot; src=&quot;data:image/png;base64,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&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
But for Python 3, pydot has some issues with the string from dot_data.getvalue(), for example it will report &quot;TypeError: startswith first arg must be str or a tuple of str, not bytes&quot;, &lt;a href=&quot;http://stackoverflow.com/questions/29172218/how-do-i-draw-decisiontree-typeerror-startswith-in-graph-from-dot-data-pydot&quot;&gt; @here&lt;/a&gt;

.&lt;br /&gt;
&lt;br /&gt;
While I was trying to modify related functions in pydot package, I figured that &quot;&lt;b&gt;pydotplus&lt;/b&gt;&quot; is a better alternative, which you can download from &lt;a href=&quot;http://pydotplus.readthedocs.org/&quot;&gt;@here&lt;/a&gt;.

&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;pre style=&quot;background-color: #ebebeb; border-image: none; border: 1px dashed rgb(153, 153, 153); color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;&quot;&gt;&lt;code&gt;
&amp;gt;&amp;gt;&amp;gt; 
&amp;gt;&amp;gt;&amp;gt; from sklearn import tree
&amp;gt;&amp;gt;&amp;gt; from sklearn.datasets import load_iris
&amp;gt;&amp;gt;&amp;gt; from sklearn.externals.six import StringIO
&amp;gt;&amp;gt;&amp;gt; 
&amp;gt;&amp;gt;&amp;gt; iris = load_iris()
&amp;gt;&amp;gt;&amp;gt; 
&amp;gt;&amp;gt;&amp;gt; classifier = tree.DecisionTreeClassifier(random_state=99999)
&amp;gt;&amp;gt;&amp;gt; 
&amp;gt;&amp;gt;&amp;gt; fitresult = classifier.fit(iris.data, iris.target)
&amp;gt;&amp;gt;&amp;gt; 
&amp;gt;&amp;gt;&amp;gt; import pydotplus
&amp;gt;&amp;gt;&amp;gt; 
&amp;gt;&amp;gt;&amp;gt; dot_data=StringIO()
&amp;gt;&amp;gt;&amp;gt; tree.export_graphviz(fitresult, out_file=dot_data)
&amp;gt;&amp;gt;&amp;gt; graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
&amp;gt;&amp;gt;&amp;gt; 
&amp;gt;&amp;gt;&amp;gt; 
&amp;gt;&amp;gt;&amp;gt; 
&amp;gt;&amp;gt;&amp;gt; graph.write_pdf(r&quot;c:\\data\\iris.pdf&quot;)
True
&amp;gt;&amp;gt;&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/9077523497121553450/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/9077523497121553450' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/9077523497121553450'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/9077523497121553450'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2015/04/sklearn-decisiontree-plot-example-needs.html' title='sklearn DecisionTree plot example needs pydotplus'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-4503422510699758380</id><published>2015-02-05T11:08:00.002-08:00</published><updated>2015-02-05T11:10:17.001-08:00</updated><title type='text'>Migrating code pieces to GitHub</title><content type='html'>One of the original reasons for this blog was to keep track of my SAS code as well as its relevant context. &lt;br /&gt;
&lt;br /&gt;


That was the mindset when I was a SAS analyst, but now working in professional software company, using the right&amp;nbsp;tool for versioning, collaborating&amp;nbsp;and code change tracking is the professional way to go.
&lt;br /&gt;
&lt;br /&gt;

So I decided to set up my own GitHub repository and will migrate my code pieces to it gradually. 

You can find my repository @ : &lt;a href=&quot;https://github.com/xieliaing/SAS&quot;&gt;https://github.com/xieliang/SAS
&lt;/a&gt;

</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/4503422510699758380/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/4503422510699758380' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/4503422510699758380'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/4503422510699758380'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2015/02/migrating-code-pieces-to-github.html' title='Migrating code pieces to GitHub'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-5162588331788212141</id><published>2014-12-18T01:30:00.000-08:00</published><updated>2015-05-25T17:45:04.401-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Data Mining"/><category scheme="http://www.blogger.com/atom/ns#" term="PROC PRINCOMP"/><category scheme="http://www.blogger.com/atom/ns#" term="SVD"/><title type='text'>%SVD macro with BY-Processing</title><content type='html'>For the Regularized Discriminant Analysis Cross Validation, we need to compute SVD for each pair of \((\lambda, \gamma)\), and the factorization result will be feed to the &lt;a href=&quot;http://www.sas-programming.com/2014/12/experient-downdating-algorithm-for.html&quot; target=&quot;_blank&quot;&gt;downdating algorithm&lt;/a&gt; to obtain leave one out variance-covariance matrix \(\hat{\Sigma}_{k\v}(\lambda, \gamma)\). 

The code is a direct modification of my original SVD macro &lt;a href=&quot;http://www.sas-programming.com/2010/03/macro-for-svd.html&quot; target=&quot;_blank&quot;&gt;@here&lt;/a&gt;. It is much more than simply put &quot;BY-&quot; statement in &lt;code&gt;PROC PRINCOMP&lt;/code&gt;, but still easy to do.

I used the within class CSSCP from the Iris data shipped with SAS and cross verified with R. &lt;a href=&quot;http://www.sas-programming.com/2010/04/conduct-r-analysis-within-sas.html&quot; target=&quot;_blank&quot;&gt;%CallR macro&lt;/a&gt; is used here.&lt;br /&gt;
&lt;br /&gt;
The screenshot below shows the SAS macro generates the same result as from R. Here is the test code.&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border: 1px dashed rgb(153, 153, 153); color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;&quot;&gt;&lt;code&gt;
dm log &#39;clear&#39;;
proc discrim data=sashelp.iris noclassify noprint
               outstat=iris_stat(WHERE=(_TYPE_ = &quot;CSSCP&quot; &amp;amp; Species ^= &quot; &quot;))
      ;
   class Species;
   var   SepalLength SepalWidth  PetalLength  PetalWidth;
run;
            
%svd_bystmt(sigma, output_v, output_s, output_u, &amp;amp;covars, Species, , nfac=0);

data _null_;
     file &quot;d:\data\sigma.csv&quot;  dlm=&quot;,&quot;;
  set iris_stat;
  if _n_=1 then do;
     put &#39;Species&#39;      &#39;,&#39; 
            &#39;_NAME_&#39;       &#39;,&#39; 
            &#39;SepalLength&#39;  &#39;,&#39; 
            &#39;SepalWidth&#39;   &#39;,&#39;   
            &#39;PetalLength&#39;  &#39;,&#39;   
            &#39;PetalWidth&#39;;
  end;
  put Species  _NAME_ SepalLength SepalWidth  PetalLength  PetalWidth;
run;

%RScript(d:\data\rscript.r)
cards4;
dat=read.csv(&quot;d:\\data\\sigma.csv&quot;, header=1, as.is=1)
head(dat)
sigma=dat[, c(-1, -2)]
sigmasvd=by(sigma, dat$Species, svd)
sigmasvd$Setosa$d
sigmasvd$Virginica$d
sigmasvd$Setosa$v
;;;;
run;

%CallR(d:/data/rscript.r, d:/data/rlog1.txt);

data _null_;
     infile &quot;d:\data\rlog1.txt&quot;;
     input;
     put _infile_;
run;
data _null_;
     do until (eof1);
        set output_s  end=eof1;
     put @1 Species= @24 Number=  @40 Singuval=  8.2;
  end;
  put &quot;             &quot;;
  do until (eof2);
     set output_v  end=eof2;
  where Species=&quot;Setosa&quot;;
  put  Prin1  8.5 Prin2 8.5 Prin3 8.5 Prin4 8.5;
  end;
  stop;
run;

&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh5D5z3-kCxReMSEtR9E0-vivBVdVxiGZIWxgYKk2GZ1B6jE3av19GvosU75u_mg75wcH53ueBmf-irx8d-YmWYDV0lnfekvosP2u_RM2ISj5_tWVk5f7lS6g3qXQpNx7ilXbf5Fg/s1600/SVD_By.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh5D5z3-kCxReMSEtR9E0-vivBVdVxiGZIWxgYKk2GZ1B6jE3av19GvosU75u_mg75wcH53ueBmf-irx8d-YmWYDV0lnfekvosP2u_RM2ISj5_tWVk5f7lS6g3qXQpNx7ilXbf5Fg/s1600/SVD_By.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border: 1px dashed rgb(153, 153, 153); color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;&quot;&gt;&lt;code&gt;
%macro SVD_bystmt(
           input_dsn,
           output_V,
           output_S,
           output_U,     
           input_vars,
    by_var,
           ID_var,
           nfac=0
           );

%local blank   para  EV  USCORE  n  pos  dsid nobs nstmt
       shownote  showsource  nbylevel  bystmt;

%let shownote=%sysfunc(getoption(NOTES));
%let showsource=%sysfunc(getoption(SOURCE));
options nonotes  nosource;

%let blank=%str( );
%let EV=EIGENVAL;
%let USCORE=USCORE;

%if &amp;amp;by_var eq &amp;amp;blank %then %do;
    %let bystmt = ␣
 %let nbylevel = 1;
%end;
%else %do;
    %let bystmt = by &amp;amp;by_var;
%end;

%let n=%sysfunc(countW(&amp;amp;input_vars));

%let dsid=%sysfunc(open(&amp;amp;input_dsn));
%let nobs=%sysfunc(attrn(&amp;amp;dsid, NOBS));
%let dsid=%sysfunc(close(&amp;amp;dsid));
%if  &amp;amp;nfac eq 0 %then %do;
     %let nstmt=␣ %let nfac=&amp;amp;n;
%end;     
%else %do;
     %let x=%sysfunc(notdigit(&amp;amp;nfac, 1)); 
  %if  &amp;amp;x eq 0 %then %do;
          %let nfac=%sysfunc(min(&amp;amp;nfac, &amp;amp;n));
          %let nstmt=%str(n=&amp;amp;nfac);
  %end;
  %else %do;
          %put ERROR: Only accept non-negative integer.;
          %goto exit;
  %end;
%end;

/* calculate U=XV/S */
%if &amp;amp;output_U ne %str() %then %do;
    %let outstmt=  out=&amp;amp;output_U.(keep=&amp;amp;by_var &amp;amp;ID_var  Prin:);
%end;
%else %do;
    %let outstmt=␣
%end;

/* build index for input data set */
proc datasets library=work nolist;
     modify &amp;amp;input_dsn;
  index create &amp;amp;by_var;
run;quit;

%let options=noint cov noprint  &amp;amp;nstmt;

proc princomp data=&amp;amp;input_dsn  
             /* out=&amp;amp;input_dsn._score */
              &amp;amp;outstmt
              outstat=&amp;amp;input_dsn._stat(where=(_type_ in (&quot;&amp;amp;USCORE&quot;, &quot;&amp;amp;EV&quot;)))  &amp;amp;options;
  &amp;amp;bystmt;
     var &amp;amp;input_vars;
run;

/* 
   need to check if the by_var is Char or Numeric, then set the
   format accordingly
*/
data _null_;
     set &amp;amp;input_dsn;
     type=vtype(&amp;amp;by_var);
  if type=&quot;C&quot; then 
     call symput(&quot;bylevelfmtvar&quot;, &quot;$bylevelfmt&quot;);
  else
     call symput(&quot;bylevelfmtvar&quot;, &quot;bylevelfmt&quot;);
run;


data __bylevelfmt;
     set &amp;amp;input_dsn._stat  end=eof;
  &amp;amp;bystmt;
  retain _nbylevel 0;
  retain  fmtname &quot;&amp;amp;bylevelfmtvar&quot;;
  if first.&amp;amp;by_var then do;
        _nbylevel+1;
  start=&amp;amp;by_var;  
  label=_nbylevel;
     output;
  keep label start fmtname  ;
  end;
  if eof then call symput(&quot;_nbylevel&quot;, _nbylevel);
run;
proc format cntlin=__bylevelfmt;
run;
%put &amp;amp;_nbylevel;


%if &amp;amp;output_V ne &amp;amp;blank %then %do;
 proc transpose data=&amp;amp;input_dsn._stat(where=(_TYPE_=&quot;&amp;amp;USCORE&quot;)) 
                out=&amp;amp;output_V.(rename=(_NAME_=variable))
                name=_NAME_;
   &amp;amp;bystmt;
      var &amp;amp;input_vars;
      id _NAME_;
      format &amp;amp;input_vars 8.6;
 run;
%end;

/* recompute Proportion */
%if &amp;amp;output_S ne %str() %then %do;
 data &amp;amp;output_S;
       set &amp;amp;input_dsn._stat ;
    &amp;amp;bystmt;
       where _TYPE_=&quot;EIGENVAL&quot;;
       array _s{*} &amp;amp;input_vars;
       array _x{&amp;amp;nfac, 3} _temporary_; 
    Total=sum(of &amp;amp;input_vars, 0);
    _t=0;
    do _i=1 to &amp;amp;nfac;
       _x[_i, 1]=_s[_i]; 
          _x[_i, 2]=_s[_i]/Total; 
       if _i=1 then 
             _x[_i, 3]=_x[_i, 2]; 
       else 
             _x[_i, 3]=_x[_i-1, 3]+_x[_i, 2];
       _t+sqrt(_x[_i, 2]);
    end;
    do _i=1 to &amp;amp;nfac;
       Number=_i;  
       EigenValue=_x[_i, 1]; Proportion=_x[_i, 2]; Cumulative=_x[_i, 3];
       S=sqrt(_x[_i, 2])/_t;  SinguVal=sqrt(_x[_i, 1] * &amp;amp;nobs/&amp;amp;_nbylevel);
       keep &amp;amp;by_var Number EigenValue  Proportion Cumulative  S SinguVal;
       output;
    end;
 run;
%end;

%if &amp;amp;output_U ne %str() %then %do; 
 data &amp;amp;output_U;
      array _S{&amp;amp;_nbylevel, &amp;amp;nfac}  _temporary_;  
      if _n_=1 then do;
         do until (eof);
            set  &amp;amp;output_S(keep=&amp;amp;by_var Number SinguVal)  end=eof;
      where Number&amp;lt;=&amp;amp;nfac;
      %if &amp;amp;by_var eq &amp;amp;blank %then %do;
          _row=1;
      %end;
      %else %do;
                   _row = input(put(&amp;amp;by_var, &amp;amp;bylevelfmtvar..), best.);
      %end;
            _S[_row, number]=SinguVal; 
            if abs(_S[_row, number]) &amp;lt; CONSTANT(&#39;MACEPS&#39;) then _S[_row, j]=CONSTANT(&#39;BIG&#39;);
         end;
     end;
     set &amp;amp;output_U;
     array _A{*}  Prin1-Prin&amp;amp;nfac;
     %if &amp;amp;by_var eq &amp;amp;blank %then %do;
        _row=1;
  %end;
        %else %do;
           _row = input(put(&amp;amp;by_var, &amp;amp;bylevelfmtvar..), best.);
  %end;
     do _j=1 to dim(_A);
         _A[_j]=_A[_j]/_S[_row, _j];
     end;
     keep &amp;amp;by_var &amp;amp;ID_var Prin1-Prin&amp;amp;nfac ;
 run;
%end;

proc datasets library=work nolist;
     modify &amp;amp;input_dsn;
  index delete &amp;amp;by_var;
run;quit;


%exit: 
options &amp;amp;shownote  &amp;amp;showsource;
%mend;
&lt;/code&gt;&lt;/pre&gt;
&lt;script src=&quot;http://latex.codecogs.com/latexit.php?dynamic&amp;amp;div&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt; 
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/5162588331788212141/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/5162588331788212141' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/5162588331788212141'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/5162588331788212141'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2014/12/svd-macro-with-by-processing.html' title='%SVD macro with BY-Processing'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh5D5z3-kCxReMSEtR9E0-vivBVdVxiGZIWxgYKk2GZ1B6jE3av19GvosU75u_mg75wcH53ueBmf-irx8d-YmWYDV0lnfekvosP2u_RM2ISj5_tWVk5f7lS6g3qXQpNx7ilXbf5Fg/s72-c/SVD_By.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-8860268406039572902</id><published>2014-12-14T22:42:00.001-08:00</published><updated>2015-05-25T17:45:47.422-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Data Mining"/><category scheme="http://www.blogger.com/atom/ns#" term="PROC DISCRIM"/><category scheme="http://www.blogger.com/atom/ns#" term="SVD"/><title type='text'>Experient downdating algorithm for Leave-One-Out CV in RDA</title><content type='html'>In this post, I want to demonstrate a piece of experiment code for downdating algorithm for Leave-One-Out (LOO) Cross Validation in Regularized Discriminant Analysis [1].
&lt;br /&gt;
&lt;br /&gt;
In LOO CV, the program needs to calculate the inverse of \(\hat{\Sigma}_{k\v}(\lambda, \gamma)\) for each observation v to obtain its new scoring function at a given pair of regularizing pair parameter \( (\lambda, \gamma) \), where \(k\v\) indicates class \(k\)
excluding observation \(v\). &lt;br /&gt;
&lt;br /&gt;
As mentioned in [1], to obtain the inverse of this variance covariance matrix for class k, it is not sufficient to just remove observation \(v\) and recalculate it, but instead, we try to calculate $$W_{k\v}(\lambda, \gamma)\hat{\Sigma}^{-1}_{k\\v}(\lambda, \gamma)$$, which is equivalent to compute the inverse of :&lt;br /&gt;
&lt;br /&gt;
\[&lt;br /&gt;
&amp;nbsp; W_k(\lambda)\hat{\Sigma}^{-1}_k (\lambda) - \gamma \|Z_v\|^2&amp;nbsp; \cdot&amp;nbsp; I - (1-\gamma)Z_v Z_v^{t} \]&lt;br /&gt;
&lt;br /&gt;
In this formula, the first element \(W_k(\lambda)\hat{\Sigma}^{-1}_k (\lambda) \)
does not involve quantities varying along with observation \(v\) and can be pre-computed. The latter two can be easily updated on the fly as we scan through each individual observations.
&lt;br /&gt;
&lt;br /&gt;
With this in mind, we use the iris data for demonstration. Note that the SAS code here is for illustrating the concept of downdating algorithm above for a given pair of [ (\lambda, \gamma) ].

The computing is divided into several steps. 

(1) We first use PROC DISCRIM to calculate CSSCP matrix, total weight and variable mean by class. These quantities correspond to \( \Sigma_k \), \( \bar{X_k}\) and \( W_k\) in the original paper.
(2) We then use the %SVD Macro &lt;a href=&quot;http://www.sas-programming.com/2010/03/macro-for-svd.html&quot; target=&quot;_blank&quot;&gt;@here&lt;/a&gt; to obtain key elements for later updating.&lt;br /&gt;
&lt;br /&gt;
Note that in order to obtain CSSCP, MEAN and total weights, in addition to PROC DISCRIM, we can also use PROC CORR. The advantage is that we can keep all computing within the framework of SAS/Base so to benefit budget-sensitive business and maximize compatibility. The down side is that the data set need to sorted or indexed to obtain CSSCP by class and the data has to be passed twice, one for pooled CSSCP and the other for class specific CSSCP.
&lt;br /&gt;
&lt;br /&gt;
&lt;strong&gt;&lt;span style=&quot;color: red;&quot;&gt;(to be completed...)&lt;/span&gt;&lt;/strong&gt;
&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border: 1px dashed rgb(153, 153, 153); color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;&quot;&gt;&lt;code&gt; 

/* study downdating algorithm */
%let lambda = 0.3;
%let gamma = 0.6;
%let target = Species;
%let covars = SepalLength SepalWidth  PetalLength  PetalWidth;

/* step 0. Obtain key statistics from DISCRIM */
proc discrim data=iris  outstat=stat noprint  noclassify;
      class ⌖
   var   &amp;amp;covars;
run;

/* step 1. Obtain \Sigma_k(\lambda) &amp;amp; \Sigma_k(\lambda, \gamma)
           \S_k = (_TYPE_=&quot;CSSCP&#39; &amp;amp; &amp;amp;target = &quot;something&quot;)
           \S  = (_TYPE_=&quot;CSSCP&quot; &amp;amp; &amp;amp;target = &quot; &quot;)
           \W_k = (_TYPE_=&quot;N&quot; &amp;amp; &amp;amp;target = &quot;something&quot;) -1
           \W  = (_TYPE_=&quot;N&quot; &amp;amp; &amp;amp;target = &quot; &quot;) - &amp;amp;nClass + 1
           \Sigma_k(\lambda) = \S_k(\lambda) / \W_k(\lambda)
              where:
                    \S_k(\lambda) = (1-\lambda)*S_k + \lambda*S
                    \W_k(\lambda) = (1-\lambda)*W_k + \lambda*W

          \Sigma_k(\lambda, \gamma) = (1-\lambda)*\Sigma_k(\lambda) + \gamma* trace[\Sigma_k(\lambda)]/p*I         
*/ 
proc sql noprint;
     select distinct &amp;amp;target into :targetvalues separated by &quot; &quot;
  from   stat
  where _TYPE_=&quot;CSSCP&quot;
  ;
  select distinct _NAME_ into :covars separated by &quot; &quot;
  from   stat
  where _TYPE_=&quot;CSSCP&quot;
  ;
quit;
%put &amp;amp;targetvalues;
%put &amp;amp;covars;

%let nClass=%sysfunc(countw(&quot;&amp;amp;targetvalues&quot;));
%put &amp;amp;nClass;
%let nVars=%sysfunc(countw(&quot;&amp;amp;covars&quot;));
%put &amp;amp;nVars;

data weights;
     set stat;
  where _TYPE_=&quot;N&quot;;
  array _nv{*} _numeric_;
  if &amp;amp;target=&quot;&quot; then _total=_nv[1]-&amp;amp;nClass+1;
  retain lambda λ
  retain _total;
  weight=(1-&amp;amp;lambda) * (_nv[1]-1) + &amp;amp;lambda*_total;  
  keep &amp;amp;target   weight  lambda;
  if &amp;amp;target ^= &quot;&quot;;
run;

proc sort data=stat  out=sigma;
      by _TYPE_  ⌖
   WHERE _TYPE_ = &quot;CSSCP&quot; &amp;amp; &amp;amp;target ^= &quot; &quot;;
run;
proc sort data=weights;
     by  ⌖
run;

/*-  step 1.1 Update \Sigma_k(\lambda) = ((1-\lambda)*S_k + \lambda*S) / ( (1-\lambda)*W_k + \lambda*W )  -*/
                                                                                          
data sigma;
     array _sigma{&amp;amp;nVars, &amp;amp;nVars} _temporary_;
  array _x{&amp;amp;nVars} &amp;amp;covars;
  array _y{&amp;amp;nClass} _temporary_;  
  if _n_=1 then do;
     _iter=1;
     do until (eof1);
        set stat(where=(_TYPE_=&quot;CSSCP&quot; &amp;amp; &amp;amp;target = &quot; &quot;))  end=eof1;
     do _j=1 to &amp;amp;nVars;
        _sigma[_iter, _j]=_x[_j];
     end;
     _iter+1;
     end; 
        
     _iter=1; 
     do until (eof2);
     set weights end=eof2;
        _y[_iter]=weight;
     _iter+1;
  end;
  put _y[3]=;
  
  _target=⌖
  _iter=0;
  end;
  modify sigma ;  
  retain  weight;
  retain  _target;
  _TYPE_=&quot;COV&quot;;
 
  if &amp;amp;target^=_target then do;
        _iter+1;
        _i=0; 
  weight=_y[_iter];
  _target=⌖
  end;
  _i+1;
  put &quot;&amp;amp;covars&quot;;
  put &amp;amp;covars;
  do _j=1 to &amp;amp;nVars;
     _x[_j]= ((1-&amp;amp;lambda)*_x[_j] + &amp;amp;lambda*_sigma[_i, _j])  ;
  end;
  put &amp;amp;covars;
  put &quot;-----------------------------------------------------------------&quot;;
  replace;  
run;

/*- trace is sum of every elemnt of a matrix -*/
data _trace;
     set sigma;
  by ⌖
  array _x{*} &amp;amp;covars;
  retain trace _iter;
  if first.&amp;amp;target then do;
     _iter = 1;
  trace=0;
  end;
  trace+_x[_iter];
  _iter+1;
  if last.&amp;amp;target then output;
  keep &amp;amp;target  trace;
run;


/*- step 1.2 Update \Sigma_k (\lambda, \gamma) = (1-\gamma)*\Sigma_k(\lambda) + \gamma *c*I -*/
/*- where  c = trace(\Sigma_k(\lambda)) / p, where p=&amp;amp;nVars                             -*/
data sigma;
      if _n_=1 then do;
      declare hash h(dataset:&#39;_trace&#39;);
   h.defineKey(&quot;&amp;amp;target&quot;);
   h.defineData(&quot;trace&quot;);
   h.defineDone(); 
   call missing(trace);
   end;
   set sigma; by ⌖
   array _x{*} &amp;amp;covars;
   retain _adj  trace _iter;
   if first.&amp;amp;target then do;
      _iter=1;
   end;
   if _iter=1 then do;
         _rc=h.find();
         _adj= &amp;amp;gamma/&amp;amp;nVars * trace;
   end;
   put _iter=  &amp;amp;target=;
   do _j=1 to &amp;amp;nVars;
      if _j=_iter then do;
      _x[_iter] = (1-&amp;amp;gamma)*_x[_iter] + _adj;
   end;
   else do;
            _x[_iter] = (1-&amp;amp;gamma)*_x[_iter];
   end;
   end;
   _iter=_iter+1;
   drop _type_   _adj _iter _j  _rc  trace;
run;



/*-step 1.3 Obtain the inverse of W_k \Sigma_k(\lambda, \gamma) - c*I  using SVD                                     -*/  
/*-This inverse is the same as first inverse \Sigma_k(\lambda, \gamma), then with S matrix deflated by W_k(\lambda)  -*/
/*-and differenced by c, where c=\gamma* |Z_v|^2/p, and Z_v = sqrt(bk(v))*(X_v- mean(X_k\v),                         -*/
/*-bk(v)=sk(v)*Wc(v) wv/(Wc(v)-wv)                                                                                   -*/
/*-v is the current observation                                                                                      -*/

/*-       1. apply SVD to obtain eigenvalues   
%SVD(sigma, out_u, out_S, out_V)                                                                                     -*/


/*-       2. The whole CV will be done in one Data Step
             Because all related quantitaties of the rest, such as such as |Z_v|, sk(v), Wc(v) 
             will require at least one pass through of the original data, therefore it is
             best to incorporate all computing in this step.
-*/
data _iris;
     array _m{*} &amp;amp;covars;
     array _mt{&amp;amp;nVars} _temporary_;
     if _n_=1 then do;
     _class=1;
        do until eof;
     set stat(where=(_TYPE_=&#39;MEAN&#39;&amp;amp; &amp;amp;target^=&quot;&quot;))  end=eof;
     do _j=1 to dim(_m);
        _mt[_class, _j]=_m[_j];
     end;
     _class+1;
  end;
  end;
  array _rank1{&amp;amp;nVars, &amp;amp;nVars} _temporary_;
  set iris point=1;  
  do _j=1 to dim(_m);
     _m[_j]=_m[_j]-_mt[1, _j];
  end;
  do _j=1 to &amp;amp;nVars;
     do _k=1 to &amp;amp;nVars;
           _rank[_j, _k]=_m[_j] * _[_k];

           /*  MORE CODE TO COME*/

     end;
  end;
run;
&lt;/code&gt;
&lt;/pre&gt;
&lt;script src=&quot;http://latex.codecogs.com/latexit.php?dynamic&amp;amp;div&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt; 

[1] J. H. Friedman. Regularized discriminant analysis. Journal of the American Statistical Association,
84(405):165–175, 1989.
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/8860268406039572902/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/8860268406039572902' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/8860268406039572902'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/8860268406039572902'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2014/12/experient-downdating-algorithm-for.html' title='Experient downdating algorithm for Leave-One-Out CV in RDA'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-7082812348335645632</id><published>2014-12-14T22:41:00.006-08:00</published><updated>2014-12-14T22:41:58.416-08:00</updated><title type='text'>Control Excel via SAS DDE &amp; Python win32com</title><content type='html'>Excel is probably the most used interface between human and data. Whenever you are dealing with business people, Excel is the de facto means for all things about data processing.
&lt;br /&gt;
&lt;br /&gt;
I used to only use SAS and Python for number crunching but in one of my recent projects, I need to work out a way to send summarized data to Excel for charting, specifically charting in the Excel flavor where the style has particularity and clients need data to be associated with the chart, just like a copied Excel chart. It is extremely difficult to fine tune your SAS or Python figures to mimic the exact required Excel style, no to mention interactive data display on chart that comes handy with Excel, so I decided to study how to control Excel via SAS and Python. &lt;br /&gt;
&lt;br /&gt;
Python is for standalone PC implementation while SAS runs on the analytic server, each serves different customer groups.

In my situation, we have MS Office 2013 Professional installed on both client PC and the server. In Python, we use &lt;a href=&quot;http://python.net/crew/mhammond/win32/Downloads.html&quot;&gt;win32com&lt;/a&gt; to control Excel while in SAS, I use the old school &lt;a href=&quot;http://support.sas.com/documentation/cdl/en/hostwin/63285/HTML/default/viewer.htm#dde.htm&quot;&gt;Dynamic Data Exchange (DDE)
&lt;/a&gt;. DDE is obsolete technology but I don&#39;t have much experience in using COM within SAS, nor did I found good examples in this aspect.&lt;br /&gt;
&lt;br /&gt;
In this blog, I show how to generate the following Excel worksheets and copy the Charts with data to PowerPoint, in Python and in SAS.&lt;br /&gt;
&lt;br /&gt;
For python scripting, I largely follow the posts from Dan&#39;s &lt;a href=&quot;http://pythonexcels.com/python-excel-mini-cookbook/&quot; target=&quot;_blank&quot;&gt;Python Excel blogs&lt;/a&gt;, and the Office IPython blog @&lt;a href=&quot;http://nbviewer.ipython.org/github/sanand0/ipython-notebooks/blob/master/Office.ipynb&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;.&amp;nbsp;You are welcome to check out his scripts there and develop your own. In order for win32com to work in your Python environment, it is required to install pywin32 package, which can be found&amp;nbsp;on its&amp;nbsp;project page&amp;nbsp;&lt;a href=&quot;http://sorceforge.net/projects/pywin32&quot; target=&quot;_blank&quot;&gt;@here&lt;/a&gt;&amp;nbsp;. You may also want to create static dispatch use &lt;span style=&quot;background-color: white;&quot;&gt;&lt;em&gt;makepy.py&lt;/em&gt;&lt;/span&gt; from the project code and installs all generated constants from a type library in the object &lt;em&gt;&lt;span style=&quot;background-color: white;&quot;&gt;win32com.client.constants&lt;/span&gt;&lt;/em&gt;.&lt;br /&gt;
&lt;br /&gt;
For SAS scripting, I largely follow SAS manual outlined above and&amp;nbsp;some SAS user papers on DDE, especially using DDECMD. But since in PowerPoint 2013 macro recording is not longer available, the work will stop at Excel.&lt;br /&gt;
&lt;br /&gt;
In the following, I will demonstrate, via comparing both languages, how to:&lt;br /&gt;
1. Invoke Excel Application&lt;br /&gt;
2. Reading data and generate contingency tables&lt;br /&gt;
3. Send the table data to Excel&lt;br /&gt;
4. Generate charts in Excel driving by SAS or Python&lt;br /&gt;
5. Copy Excel charts to PowerPoint with data embedded.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;span style=&quot;color: red;&quot;&gt;&lt;strong&gt;(to be completed .. )&lt;/strong&gt;&lt;/span&gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border: 1px dashed rgb(153, 153, 153); color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;&quot;&gt;&lt;code&gt;


&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/7082812348335645632/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/7082812348335645632' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/7082812348335645632'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/7082812348335645632'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2014/12/control-excel-via-sas-dde-python.html' title='Control Excel via SAS DDE &amp; Python win32com'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-7663061815026654061</id><published>2014-07-01T10:52:00.000-07:00</published><updated>2014-08-12T09:56:29.245-07:00</updated><title type='text'>%HPGLIMMIX SAS macro is available online at JSS website</title><content type='html'>My paper &quot;%HPGLIMMIX: A High-Performance SAS Macro for GLMM Estimation&quot; is now available at Journal of Statistical Software website &lt;a href=&quot;http://www.jstatsoft.org/v58/i08&quot; target=&quot;_blank&quot;&gt;@here&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
SAS macro and code can also be found there. If you use it, please kindly send me an email so that I know my work is appreciated and is helpful to more people.&lt;br /&gt;
===========&lt;br /&gt;
&lt;br /&gt;
Update:&lt;br /&gt;
&lt;br /&gt;
The paper and the macro&amp;nbsp;are well accepted: 400+ downloads for the paper and 90+ downloads for the macro for&amp;nbsp;the first&amp;nbsp;month it is online and I&#39;ve got many good feedbacks. Several key points to make:&lt;br /&gt;
&lt;br /&gt;
1. For binary dependent variable, you have to code your data into numeric {0, 1} and the macro will model the probability dependent variable having value 1. So if you have other requirements, you have to process the data before calling the macro.&lt;br /&gt;
&lt;br /&gt;
2. This macro relies on PROC HPMIXED, so that it will generate two residual&amp;nbsp;parameters by default, therefore for&amp;nbsp;conditional distributions without&amp;nbsp;scale parameters, such as Poisson,&amp;nbsp;users need to&amp;nbsp;hold the&amp;nbsp;last residual variance to be 1 by using PARMS /HOLD=&amp;nbsp;statement. Please see the explanation of second difference between this macro and PROC GLIMMIX on Page 9, as well as Example 2.</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/7663061815026654061/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/7663061815026654061' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/7663061815026654061'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/7663061815026654061'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2014/07/hpglimmix-sas-macro-is-available-online.html' title='%HPGLIMMIX SAS macro is available online at JSS website'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-4901642983278738521</id><published>2013-12-06T00:16:00.004-08:00</published><updated>2014-02-21T09:32:36.443-08:00</updated><title type='text'>Market trend in advanced analytics for SAS, R and Python</title><content type='html'>Disclaimer: This study is a view on the market trend&amp;nbsp;on demand of advanced analytics software and their adoptions from the job market perspective, and should not be read as a conclusive statement on what is all happening there. The findings should be combined with other third party research results such IDC reports &lt;a href=&quot;http://www.idc.com/getdoc.jsp?containerId=241689&quot;&gt;http://www.idc.com/getdoc.jsp?containerId=241689&lt;/a&gt;&amp;nbsp;to reach a balanced and comprehensive idea.&lt;br /&gt;
&lt;br /&gt;
/* --------------------------------------------------------------------------------------------------------------------*/&lt;br /&gt;
The debate on the competition between SAS, R and Python for advanced analytics, Statistics or Data Science in general never ends. The question boils down to how far SAS, as a software and programming language for such purpose, can still be used by business in mass, given its&amp;nbsp;current pricing structure.&lt;br /&gt;
&lt;br /&gt;
Some recent discussions can be found at:&lt;br /&gt;
&lt;br /&gt;
&lt;a href=&quot;http://www.listserv.uga.edu/cgi-bin/wa?A2=ind1312a&amp;amp;L=sas-l&amp;amp;F=&amp;amp;S=&amp;amp;P=1171&quot;&gt;http://www.listserv.uga.edu/cgi-bin/wa?A2=ind1312a&amp;amp;L=sas-l&amp;amp;F=&amp;amp;S=&amp;amp;P=1171&lt;/a&gt;&lt;br /&gt;
&lt;br /&gt;
&lt;a href=&quot;http://r4stats.com/2013/03/19/r-2012-growth-exceeds-sas-all-time-total/&quot;&gt;http://r4stats.com/2013/03/19/r-2012-growth-exceeds-sas-all-time-total/&lt;/a&gt;&lt;br /&gt;
&lt;br /&gt;
&lt;a href=&quot;http://r4stats.com/articles/popularity/&quot;&gt;http://r4stats.com/articles/popularity/&lt;/a&gt;&lt;br /&gt;
&lt;br /&gt;
and more...&lt;br /&gt;
&lt;br /&gt;
indeed.com has been used to address such question before by &lt;a href=&quot;http://r4stats.com/&quot; target=&quot;_blank&quot;&gt;Robert Muenchen&lt;/a&gt; @ &lt;a href=&quot;http://r4stats.com/articles/popularity/&quot;&gt;http://r4stats.com/articles/popularity/&lt;/a&gt;. His analysis on popularity of SAS and R in the job market using indeed.com was simple. The terms he used were just &quot;R SAS&quot; and &quot;SAS !SATA !storage !firmware&quot;.&lt;br /&gt;
&lt;br /&gt;
In my analysis using indeed trend, I directly search combination of languages such as R, SAS and Python with analytics related job terms, such as &quot;R AND Analytics&quot; or &quot;R AND Regression&quot;, etc. The goal is to understand the market dynamics in adopting each of the three languages in advanced business analytics. I try to be as fair as possible in my analysis.&lt;br /&gt;
&lt;br /&gt;
The job description related terms that I am going to combine with the language names fall into three categories:&lt;br /&gt;
&lt;br /&gt;
1. Techniques typically used, here I use &quot;Regression&quot;, which almost all advanced business analytics job will look for;&lt;br /&gt;
2. Industry, here I use &quot;pharmaceutical&quot;, and I used &quot;JPMorgan Chase&quot; to represent financial service (even though it is not representative&quot;&lt;br /&gt;
3. General term such as &quot;data mining&quot;, &quot;machine learning&quot; and &quot;statistics&quot;. In general, data mining has different implication from machine learning. The former is a more general term, or a modern term for &quot;analytics&quot; while the latter shows up momentum only recently and largely focus on computer science related field and more hardcore on algorithm development, etc, so favoring Python over the other two.&lt;br /&gt;
&lt;br /&gt;
The graph below shows indeed.com job trends using search term &quot;R and regression, SAS and regression, Python and regression&quot;. It has three immediate pieces of information to tell:&lt;br /&gt;
a.) Python has been picking up very fast since 09;&lt;br /&gt;
b.) The market share gap between SAS and R/Python is consistently dropping since 2006 and in foreseeable future the lead of SAS is going to disappear.&lt;br /&gt;
c.) SAS&#39;s market share reached high peak in 2011 but&amp;nbsp;kept dropping&amp;nbsp;thereafter while Python maintains steady trend and R is picking up in the same time period.&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhEabeShY_-c2Oc4XLI19D9DL_umN_EuU0Wxiz9sLy5WgvlpGA619lMTo2HOer5Htx-KCSfiDHAg_f0f2CiosmNX-ivuDPqpKibK8kVhQJ1P4sPCVapcJnVlDP6obMlhsum6B8pug/s1600/Regression.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhEabeShY_-c2Oc4XLI19D9DL_umN_EuU0Wxiz9sLy5WgvlpGA619lMTo2HOer5Htx-KCSfiDHAg_f0f2CiosmNX-ivuDPqpKibK8kVhQJ1P4sPCVapcJnVlDP6obMlhsum6B8pug/s640/Regression.png&quot; height=&quot;353&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The below graph searches the trend using term &quot;R and pharmaceutical&quot;, etc. I wanted to see how the trend of adopting those three languages in pharmaceutical industry. What we found out are three points:&lt;br /&gt;
a.) R and SAS almost show up at the same time in pharmaceutical jobs&lt;br /&gt;
b.) The relative job demand for analytics in pharmaceutical industry is declining.&lt;br /&gt;
c.) Python is on the rise even though still trailing far behind&amp;nbsp;SAS and R.&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiTQETDGZnvwLxfw1Jhz4tP5-YVs3FFbr7tiqo5oH_mjd12zSoPdY7hVd3VCJfBd_WzAIBpS4HS96AkloAu01lZJiNn9WYFCYgQBIXwqu8zCYPFraoJB1lEgic_v_IodE1-MCEaIA/s1600/pharmacuetical.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiTQETDGZnvwLxfw1Jhz4tP5-YVs3FFbr7tiqo5oH_mjd12zSoPdY7hVd3VCJfBd_WzAIBpS4HS96AkloAu01lZJiNn9WYFCYgQBIXwqu8zCYPFraoJB1lEgic_v_IodE1-MCEaIA/s640/pharmacuetical.png&quot; height=&quot;353&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
When using &quot;JPmorgan Chase&quot; combined with languages for trend analysis, I specifically excluded &quot;storage&quot; from the SAS search term because the bank may hire some IT folks and result will mix with SAS storage system. The term is &#39; R and &quot;jpmorgan chase&quot;, SAS not Storage and &quot;jpmorgan chase&quot;, Python and &quot;jpmorgan chase&quot; &#39;. There are several interesting observations:&lt;br /&gt;
a.) Python is picking up some job shares in JPmorgan Chase from 2010. R is losing;&lt;br /&gt;
b.) There is noticeable seasonality in hiring, mostly in summer time;&lt;br /&gt;
c.) Before 2008, there was strong demand for analysts using SAS, then the crisis came and hiring stalled for almost 2 years. Beginning in 2011, the business is getting better and the demand for analysts using SAS is picking up all year around and attained peak in this year.&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjA1lchNbRJPfw8zc0_-qtOWDXdoG_M5trzGi0zieDdrRcUk3Lfih9Z1iaNbC24nIxB3H8I92bPwDEESTL3dXMYJiKuXpqTwZiHQ1-6HNioE1ASWwTP-aFWpPBkfIGMnw6TbuERxQ/s1600/jpmorgan+chase.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjA1lchNbRJPfw8zc0_-qtOWDXdoG_M5trzGi0zieDdrRcUk3Lfih9Z1iaNbC24nIxB3H8I92bPwDEESTL3dXMYJiKuXpqTwZiHQ1-6HNioE1ASWwTP-aFWpPBkfIGMnw6TbuERxQ/s640/jpmorgan+chase.png&quot; height=&quot;353&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
In our last part of trend analysis, we want to combine the languages with more general terms such as &quot;analytics&quot;, &quot;data mining&quot; to see how the whole business perceives these three analytical languages.&lt;br /&gt;
&lt;br /&gt;
We first combine with &quot;data mining&quot;. Three points need to be noted:&lt;br /&gt;
1. The job market for analytics in general has been increasing since 2009 and all languages shows good momentum from 2009 to 2010;&lt;br /&gt;
2. SAS was the dominant player in this business, but the landscape is changing rapidly. Both R and Python see almost identical adoption by the business. There are some minimal gap between these two but I would consider it not significant;&lt;br /&gt;
3. SAS is not as favored by the market as before and its share not only stalled but also showed some declining trend this year. This is very alarming.&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjU5Ydv5eK0flq8gsf1UfKOo96lPJCBynoAXIru0B7WFptPzVLteTKFQKFars1eDsuMWFb462ty5uXTJvK0hta_-e2oiV7-IW2Bk8yXkSwUy93mD1i3sADjJp1iUckFpk25E9ex8A/s1600/data+mining.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjU5Ydv5eK0flq8gsf1UfKOo96lPJCBynoAXIru0B7WFptPzVLteTKFQKFars1eDsuMWFb462ty5uXTJvK0hta_-e2oiV7-IW2Bk8yXkSwUy93mD1i3sADjJp1iUckFpk25E9ex8A/s640/data+mining.png&quot; height=&quot;353&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
Now, let&#39;s be more focused. First, we study &quot;Statistics&quot;. The message is not good for SAS here. SAS used to be THE &lt;i&gt;de facto&lt;/i&gt; software for industrial statistical analysis. This fact is reflected by the trend before 2010. SAS is so dominant that its market share is more than 3X comparing to R and even more comparing to Python. But what&#39;s interesting is that while SAS saw some good days from 2010 to 2011, which also reflected in their annual revenue&quot;, its market share is on a steady down road from 2012. On the other hand, R and Python are still picking up without hesitation.&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgsKOjQMah1hkgPzEEYzIjybVb89bMMfWxzKDbGf_MAfN_U6EtH3z1-ChVox-RQ41_pyHw5CUcACtIzEDvNwOW-KU-GthSQsMZVDMFo2g9kAS3j3BpyuIjeftcqZ1LykZEH7Ue13g/s1600/statistics.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgsKOjQMah1hkgPzEEYzIjybVb89bMMfWxzKDbGf_MAfN_U6EtH3z1-ChVox-RQ41_pyHw5CUcACtIzEDvNwOW-KU-GthSQsMZVDMFo2g9kAS3j3BpyuIjeftcqZ1LykZEH7Ue13g/s640/statistics.png&quot; height=&quot;353&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Turning to &quot;machine learning&quot;, Python is the leader, followed by R.  SAS sees some distance here. The seasonality of Python is a little different from R and SAS while the latter two have very close seasonal dynamics. The hypothesis here is that Python is dominantly used in different industries while R and SAS share similar industries.&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWJHaXwRqr36w6C03T5CAel8IarUykyWphnhPuXOlpmKH_NTl2B7iiK0SEVyhFce0D1D1Gk0xqCJf5bqEkttS_TuIqebRrnb0G0lLa2kYYraQz_rbKjX5enYuNZ9eQ3Q5DNvUqzg/s1600/machine+learning.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWJHaXwRqr36w6C03T5CAel8IarUykyWphnhPuXOlpmKH_NTl2B7iiK0SEVyhFce0D1D1Gk0xqCJf5bqEkttS_TuIqebRrnb0G0lLa2kYYraQz_rbKjX5enYuNZ9eQ3Q5DNvUqzg/s640/machine+learning.png&quot; height=&quot;353&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/4901642983278738521/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/4901642983278738521' title='5 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/4901642983278738521'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/4901642983278738521'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2013/12/sas-is-abandend-by-market-for-advanced.html' title='Market trend in advanced analytics for SAS, R and Python'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhEabeShY_-c2Oc4XLI19D9DL_umN_EuU0Wxiz9sLy5WgvlpGA619lMTo2HOer5Htx-KCSfiDHAg_f0f2CiosmNX-ivuDPqpKibK8kVhQJ1P4sPCVapcJnVlDP6obMlhsum6B8pug/s72-c/Regression.png" height="72" width="72"/><thr:total>5</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-4357462377594027338</id><published>2013-07-18T21:39:00.000-07:00</published><updated>2013-07-19T19:06:00.628-07:00</updated><title type='text'>I don&#39;t always do regression, but when I do, I do it in SAS 9.4</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiY0H3LTL0BJqGWkgC4cPsL6X-2HKbKx4Me23467NtbHLdEFN_i8wUFTZ-y0fgGyTck8u8cRoKfGwALNne6S8tIogpKwVf-Jpp9V-ml_sq24m7NR62B9xCpds3kAp99v0wq2Shgbg/s1600/dos-equis-man-SAS.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;640&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiY0H3LTL0BJqGWkgC4cPsL6X-2HKbKx4Me23467NtbHLdEFN_i8wUFTZ-y0fgGyTck8u8cRoKfGwALNne6S8tIogpKwVf-Jpp9V-ml_sq24m7NR62B9xCpds3kAp99v0wq2Shgbg/s640/dos-equis-man-SAS.jpg&quot; width=&quot;510&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
There are several exciting add-ins from SAS Analytics products running on v9.4, especially the SAS/STAT high performance procedures, where &quot;high performance&quot; refers to either in single-machine multi-threading mode or full distributed mode.&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;&lt;b&gt;HPGENSELECT:&lt;/b&gt; high performance model fitting and variable selection for GLM within standard exponential family;&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;&lt;b&gt;HPLOGISTIC:&lt;/b&gt; high performance logistic regression&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;&lt;b&gt;HPLMIXED: &lt;/b&gt;high performance Linear Mixed Model, unfortunately it still strictly a LMM, so does not support the DIST= option in MODEL statement&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;&lt;b&gt;HPNLMOD:&lt;/b&gt; high performance nonlinear LS or MLE&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;&lt;b&gt;HPREG:&lt;/b&gt; high performance for OLS, luckily HPREG supports CLASS statement and includes LAR/LASSO variable selection methods. On the other hand, HPREG does not supports best-subsets variable selection methods.&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;&lt;b&gt;HPSPLIT:&lt;/b&gt; high-performance utility procedure that creates a decision tree model and saves results in output data sets and files for use in SAS Enterprise Miner.
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/4357462377594027338/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/4357462377594027338' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/4357462377594027338'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/4357462377594027338'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2013/07/i-dont-always-do-regression-but-when-i.html' title='I don&#39;t always do regression, but when I do, I do it in SAS 9.4'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiY0H3LTL0BJqGWkgC4cPsL6X-2HKbKx4Me23467NtbHLdEFN_i8wUFTZ-y0fgGyTck8u8cRoKfGwALNne6S8tIogpKwVf-Jpp9V-ml_sq24m7NR62B9xCpds3kAp99v0wq2Shgbg/s72-c/dos-equis-man-SAS.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-2027233477258049134</id><published>2013-05-08T20:58:00.003-07:00</published><updated>2013-05-22T20:36:41.681-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Nearest Neighbor"/><category scheme="http://www.blogger.com/atom/ns#" term="PROC DISTANCE"/><category scheme="http://www.blogger.com/atom/ns#" term="PROC MODECLUS"/><title type='text'>Finding the closest pair in datat using PROC MODECLUS</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhAV_UyAUkGpHce_t6p2OHgWDk-hkp-0pTJIfn5DgmW2kbmLrcNhb0MK7wjryQPR6LCOnMaOT6yjKPnDDEZ162dLKb6IxyrNz3YXrlOU0j4sC22QYcvkocCsFwkpCPjK227qeY7Wg/s1600/SGPlot4.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;300&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhAV_UyAUkGpHce_t6p2OHgWDk-hkp-0pTJIfn5DgmW2kbmLrcNhb0MK7wjryQPR6LCOnMaOT6yjKPnDDEZ162dLKb6IxyrNz3YXrlOU0j4sC22QYcvkocCsFwkpCPjK227qeY7Wg/s400/SGPlot4.png&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: left;&quot;&gt;
&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: left;&quot;&gt;
&lt;strong&gt;UPDATE:&lt;/strong&gt; &lt;a href=&quot;http://blogs.sas.com/content/iml&quot; target=&quot;_blank&quot;&gt;Rick Wicklin&lt;/a&gt; kindly shared his &lt;a href=&quot;http://www.listserv.uga.edu/cgi-bin/wa?A2=ind1305b&amp;amp;L=sas-l&amp;amp;X=10A9B16FBC041F1D71&amp;amp;Y=dynamicpanel@yahoo.com&amp;amp;P=4852&quot; target=&quot;_blank&quot;&gt;visualization efforts&lt;/a&gt; on the output to put a more straightforward sense on the results. Thanks. Here is the code, run after my code below. Note that this is designed for K=2.&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: left;&quot;&gt;
&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: left;&quot;&gt;
&lt;code&gt;&lt;/code&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: left;&quot;&gt;
proc iml;&lt;/div&gt;
use out;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; read all var {ID x y}; close out;&lt;br /&gt;
use neighbor; read all var {Nbor}; close neighbor;&lt;br /&gt;
NBor = num(Nbor);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; /* convert to numeric */&lt;br /&gt;
xEnd = x[Nbor];&lt;br /&gt;
yEnd = y[Nbor];&lt;br /&gt;
create Viz var {ID x y xEnd yEnd}; append; close Viz;&lt;br /&gt;
quit;&lt;br /&gt;
proc sgplot data=Viz noautolegend;
   &lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; scatter y=y x=x / datalabel=id;
&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp; vector x=xEnd y=yEnd / xorigin=x yOrigin=y;
&lt;br /&gt;
run;
&lt;br /&gt;
&lt;br /&gt;
***************************************************************************&lt;br /&gt;
&lt;div align=&quot;left&quot; class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
﻿&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWFsR9nHpja4dZnOv0QWZ8HH0pTO2o-m5NN80KQxfpgbUiTJ8iyKPwVIgOgbdVVraf_wzlloLb527tTwvIsb2gglagDIOqtC_JeGESUJPtv6LaUROxfGS-eJiXoDiXVsBJthLQ5A/s1600/modeclus.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;307&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWFsR9nHpja4dZnOv0QWZ8HH0pTO2o-m5NN80KQxfpgbUiTJ8iyKPwVIgOgbdVVraf_wzlloLb527tTwvIsb2gglagDIOqtC_JeGESUJPtv6LaUROxfGS-eJiXoDiXVsBJthLQ5A/s400/modeclus.png&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
More often than not, the analyst wants to find the pair of observations that&amp;nbsp;are the&amp;nbsp;closest in terms of certain metrics, such as Euclidean distance. For example, on the popular SAS-L archive group, Randall Powers posted such a question at &lt;a href=&quot;http://www.listserv.uga.edu/cgi-bin/wa?A2=ind1305a&amp;amp;L=sas-l&amp;amp;F=&amp;amp;S=&amp;amp;P=14817&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;, but he was using PROC DISTANCE.&lt;br /&gt;
&lt;br /&gt;
His idea is straightforward: First calculate the pairwise distance among all observations, and Second, find the one that has the shortest distance. For the first step, he was trying to use PROC DISTANCE, then then he planned to use DATA STEP to search through. This is not workable in even the modest data size, say 100K observations, because PROC DISTANCE will generate a dense double precision 100K-by-100K matrix and will blow up your workstation. That is the reason Randall turns to &lt;a href=&quot;http://www.listserv.uga.edu/archives/sas-l.html&quot; target=&quot;_blank&quot;&gt;SAS-L&lt;/a&gt;&amp;nbsp;for help.&lt;br /&gt;
&lt;br /&gt;
Well, SAS actually has an old PROC that can readily solve his problem. Of course, before a solution can be crafted, there are several issues that have to be clarified first.&lt;br /&gt;
&lt;br /&gt;
First, what if some observations share the same closest observations, do you allow shared case or you need a mutually exclusive solution that each observation only has its unique non-shared counterpart as the closest point, then how would you determine which one should take the shared observation? First come first serve or any other rule?&lt;br /&gt;
&lt;br /&gt;
Second, when we talk about pairs, we know N should be even number, but what if the data we have has an odd number for N? How to deal with the left over?&lt;br /&gt;
&lt;br /&gt;
That being said, let&#39;s consider the simplest and most tolerate case, that is each observation is allowed to serve as the closest point to multiple other observations and N is an even number.&lt;br /&gt;
&lt;br /&gt;
The solution uses &lt;b&gt;&lt;a href=&quot;http://support.sas.com/documentation/cdl/en/statug/63033/HTML/default/viewer.htm#modeclus_toc.htm&quot; target=&quot;_blank&quot;&gt;PROC MODECLUS&lt;/a&gt;. &lt;/b&gt;This is an old procedure for density based clustering analysis, but its build-in nearest neighbor search algorithm and output capability make it the perfect candidate for this type of job. We use the example data in the &quot;Getting Started&quot; section.&lt;br /&gt;
&lt;br /&gt;
The ODS OUTPUT statement directly outputs the list of nearest neighbors (closest points) for each observation and your have to specify either &lt;b&gt;ALL&lt;/b&gt; or&lt;b&gt; NEIGHBOR&lt;/b&gt; option in the PROC MODECLUS statement in order to use this functionality. In the same statement, we also specify &lt;b&gt;K=&lt;/b&gt;2. K=2 means 2 nearest neighbor but since this applies for all observations, for each one, the nearest neighbor is K-1=1. So if you specify K=3, you actually ask this procedure find 2 closest point for each 1 observation at hand.&lt;br /&gt;
&lt;br /&gt;
The good thing about the nearest neighbor list data set is that it also contains the calculated distance, therefore, in cases you need to deal with more complex situation as listed above, such as non-shared nearest neighbor, etc, you need some room for post processing. Here you can specify K=3 and PROC MODECLUS will output 2 neighbors for each observation, like below.&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi1mxe3BZp1dupeTRnVOjpaiIUcSj6tW3QADgRFA-Acnwa8axN0vhjhDV4tAbhfcswEIKrII1HWQJLUfRIQZijSuKQVllBjYv8UIAJvks-pKiNsKaM2NobqTZJy-6NuHezWNCvZ5Q/s1600/NN1.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;208&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi1mxe3BZp1dupeTRnVOjpaiIUcSj6tW3QADgRFA-Acnwa8axN0vhjhDV4tAbhfcswEIKrII1HWQJLUfRIQZijSuKQVllBjYv8UIAJvks-pKiNsKaM2NobqTZJy-6NuHezWNCvZ5Q/s400/NN1.png&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
You can see that observation 3 can be matched to either observation 1 or 2, but with 2, it yields the shortest distance, and if this is the rule you are going to apply, your post process can work on this data set to implement the rule.&lt;br /&gt;
&lt;br /&gt;
Please feel free to verify the result. If you find error, let me know or post in the comment.&lt;br /&gt;
&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border: 1px dashed rgb(153, 153, 153); color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding: 5px; width: 100%;&quot;&gt;&lt;code&gt; 
data example;
      input x y @@;
   ID=_n_;
      datalines;
   
   18 18  20 22  21 20  12 23  17 12  23 25  25 20  16 27
   20 13  28 22  80 20  75 19  77 23  81 26  55 21  64 24
   72 26  70 35  75 30  78 42  18 52  27 57  41 61  48 64
   59 72  69 72  80 80  31 53  51 69  72 81
   ;
run;

ods select none;
ods output neighbor=neighbor; /* ODS output dataset */
proc modeclus data=example method=0  k=2    /*Find a pair of nearest neighbor*/
                 out=out  
                 all  /* required option in order to output Nearest Neighbors*/;
     var x y;
  id  id;
run;
ods select all;


&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/2027233477258049134/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/2027233477258049134' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/2027233477258049134'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/2027233477258049134'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2013/05/finding-closest-pair-in-dataset-using.html' title='Finding the closest pair in datat using PROC MODECLUS'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhAV_UyAUkGpHce_t6p2OHgWDk-hkp-0pTJIfn5DgmW2kbmLrcNhb0MK7wjryQPR6LCOnMaOT6yjKPnDDEZ162dLKb6IxyrNz3YXrlOU0j4sC22QYcvkocCsFwkpCPjK227qeY7Wg/s72-c/SGPlot4.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-9141423847195423818</id><published>2013-03-25T21:44:00.000-07:00</published><updated>2013-12-08T00:05:09.476-08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="PROC HPMIXED"/><title type='text'>Large Scale Linear Mixed Model</title><content type='html'>Update at the end:&lt;br /&gt;
****************************;&lt;br /&gt;
Bob at &lt;a href=&quot;http://r4stats.com/&quot;&gt;r4stats.com&lt;/a&gt; &lt;a href=&quot;http://r4stats.com/&quot;&gt;&lt;/a&gt; claimed &lt;a href=&quot;http://r4stats.com/2013/03/19/r-2012-growth-exceeds-sas-all-time-total/&quot; target=&quot;_blank&quot;&gt;that&lt;/a&gt; a linear mixed model with over 5 million observations and 2 million levels of random effects was fit using lme4 package in R:&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiaLhQX-B4hwW2zNZcYe3swQcZ6mycjdG4wB4jsAnzie270zOtzDZMRsfpnb3cnzDySGtwRAQYN80FJygKL2cXIFI72fMZC0uMB8t9fBfrsSKf1p7LPvrHmrU06EH59MxgUuCX2zw/s1600/lme4eg.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;640&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiaLhQX-B4hwW2zNZcYe3swQcZ6mycjdG4wB4jsAnzie270zOtzDZMRsfpnb3cnzDySGtwRAQYN80FJygKL2cXIFI72fMZC0uMB8t9fBfrsSKf1p7LPvrHmrU06EH59MxgUuCX2zw/s640/lme4eg.png&quot; width=&quot;539&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
I am always interested in large scale mixed model like this and&lt;b&gt; would appreciate anyone who can point me to the example Bob quoted (update: Bob responded in comment, please check it out)&lt;/b&gt;. &lt;strike&gt;I left a reply to Bob on his blog but no response yet.&lt;/strike&gt; I also tried to Google this example but all examples returned are on a scale with hundreds up to 5 thousand of levels of random effects, which is very common and reasonable. 

While I don&#39;t have the data used in the quoted example, I decided to see how long it takes for a mixed model with similar scale to be fitted in SAS&#39;s PROC HPMIXED. Therefore I borrowed Tianlin Wang&#39;s simulated data example in his paper: &lt;a href=&quot;http://support.sas.com/resources/papers/proceedings09/256-2009.pdf&quot; target=&quot;_blank&quot;&gt;All the Cows in Canada&lt;/a&gt;: Massive Mixed Modeling with the HPMIXED procedure in SAS 9.2. But I modified the code a little bit so that there will be &lt;span style=&quot;color: red;&quot;&gt;&lt;b&gt;400K cows &lt;/b&gt;&lt;/span&gt;to be included in the random effect and I used SUBJECT= option to accelerate the computing with by-subject processing.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
%let NFarm = 1000;&lt;br /&gt;
%let NAnimal = %eval(&amp;amp;NFarm*400);&lt;br /&gt;
%let Va = 4.0;&lt;br /&gt;
%let Ve = 8.0;&lt;br /&gt;
data Sim;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; keep Species Farm Animal Yield;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; array BV{&amp;amp;NAnimal};&lt;br /&gt;
&amp;nbsp; &amp;nbsp; array AnimalSpecies{&amp;amp;NAnimal};&lt;br /&gt;
&amp;nbsp; &amp;nbsp; array AnimalFarm{&amp;amp;NAnimal};&lt;br /&gt;
&amp;nbsp; &amp;nbsp; do i = 1 to &amp;amp;NAnimal;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;BV {i} = sqrt(&amp;amp;Va)*rannor(12345);&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;AnimalSpecies{i} = 1 + int( 5 *ranuni(12345));&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;AnimalFarm {i} = 1 + int(&amp;amp;NFarm*ranuni(12345));&lt;br /&gt;
&amp;nbsp; &amp;nbsp; end;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; do i = 1 to 40*&amp;amp;NAnimal;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;Animal = 1 + int(&amp;amp;NAnimal*ranuni(12345));&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;Species = AnimalSpecies{Animal};&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;Farm = AnimalFarm {Animal};&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;Yield = 1 + Species&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;+ Farm&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;+ BV{Animal}&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;+ sqrt(&amp;amp;Ve)*rannor(12345);&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;output;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; end;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
proc sort data=sim; by animal species farm; run;&lt;br /&gt;
options fullstimer;&lt;br /&gt;
ods listing close;&lt;br /&gt;
ods select none;&lt;br /&gt;
proc hpmixed data=Sim;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;class Species Farm Animal;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;model Yield = Species Farm*Species /ddfm=residual;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;random animal /cl ;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;ods output SolutionR=EBV;&lt;br /&gt;
run;&lt;br /&gt;
ods listing;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
For this particular model, it took more than&amp;nbsp;&lt;b&gt;8 hours&lt;/b&gt; to be fitted on a desktop with i5-3570K CPU running at 3.8Ghz. I certainly dare not to test against a case with 2 million levels of random effects and would assume it will be a LONG LONG TIME. Note that with the original 10K levels of random effects, it took only &lt;b&gt;20 seconds&lt;/b&gt; to be fitted on the same machine, and I do know the time consumption is more than super-linear in the number of random effects&#39; levels with all the computing optimization in HPMIXED.&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-p0FtrGzTURgjttgPBbCOqCriyUH2A0Nv97nVaAJ4I66R2IysrHvqmo1LJlYjtTZSMhK_LB1yrTPMH8nLrZhp3x74T2hjto7sON1_Se4QVx6BtTQ0qDG1aWHDWRA3HJo8SxIKZQ/s1600/examplerun.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;640&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-p0FtrGzTURgjttgPBbCOqCriyUH2A0Nv97nVaAJ4I66R2IysrHvqmo1LJlYjtTZSMhK_LB1yrTPMH8nLrZhp3x74T2hjto7sON1_Se4QVx6BtTQ0qDG1aWHDWRA3HJo8SxIKZQ/s640/examplerun.png&quot; width=&quot;576&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;/div&gt;
&lt;br /&gt;
&lt;i&gt;lme4&lt;/i&gt; package supports Sparse matrix techniques only on the random effect design matrix, but HPMIXED procedure supports Sparse matrix techniques to the full Mixed Model Equation, and in many applications, will perform much better:&lt;br /&gt;
&lt;br /&gt;
Take the above example, but using only 150 Farms and assuming 100 Animals within a Farm, we fit the same mixed model using&lt;i&gt; lme4&lt;/i&gt; package and the HPMIXED procedure. The &lt;b&gt;lmer&lt;/b&gt; function is called within SAS using the &lt;b&gt;%RScript&lt;/b&gt; and &lt;b&gt;%CallR&lt;/b&gt; macros (see&lt;a href=&quot;http://www.sas-programming.com/2010/04/conduct-r-analysis-within-sas.html&quot; target=&quot;_blank&quot;&gt; here&lt;/a&gt;).&lt;br /&gt;
&lt;br /&gt;
Left panel shows the status of the machine running &lt;b&gt;lmer&lt;/b&gt; function on the mixed model and the right panel shows the status when the machine ran HPMIXED. Because &lt;i&gt;lme4&lt;/i&gt; does not support sparse matrix techniques for fixed effect design matrix, it takes significant penalty on memory usage (12+GB vs 25MB) and computing time (244sec real time vs. 17sec real time). In a later test using only random effects, HPMIXED used 5.7 seconds while &lt;b&gt;lmer&lt;/b&gt; used 11.3 second.&lt;br /&gt;
&lt;br /&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhqa6eP92hhqGALd1l8viSeiFJfK7ysdnWsD8kRGtBaEeHz-Ia57n348lXaSIWuCIUqeXUAhVXqmnpm5UPAubpDkBoeZR4kbiU0FiJCJbyk82mhumtvxl4K1Vkvw0H5ZP7vq4XUBw/s1600/rsas.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;392&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhqa6eP92hhqGALd1l8viSeiFJfK7ysdnWsD8kRGtBaEeHz-Ia57n348lXaSIWuCIUqeXUAhVXqmnpm5UPAubpDkBoeZR4kbiU0FiJCJbyk82mhumtvxl4K1Vkvw0H5ZP7vq4XUBw/s640/rsas.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Left: Running R; Right: Running SAS&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjlGM_H_WChzdjtTFC7JbRUQPLRK_WFPCFFbdQwa_juA7Wike2ddK6zfxilE97mflxS5vAoC4K2nysmwnMVxH-QCtCHKJx0gwF4YcYPSrPBLGd7ujpWJrxSn5HrNMi2ksTO0EyA_w/s1600/log.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjlGM_H_WChzdjtTFC7JbRUQPLRK_WFPCFFbdQwa_juA7Wike2ddK6zfxilE97mflxS5vAoC4K2nysmwnMVxH-QCtCHKJx0gwF4YcYPSrPBLGd7ujpWJrxSn5HrNMi2ksTO0EyA_w/s1600/log.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/9141423847195423818/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/9141423847195423818' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/9141423847195423818'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/9141423847195423818'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2013/03/large-scale-linear-mixed-model.html' title='Large Scale Linear Mixed Model'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiaLhQX-B4hwW2zNZcYe3swQcZ6mycjdG4wB4jsAnzie270zOtzDZMRsfpnb3cnzDySGtwRAQYN80FJygKL2cXIFI72fMZC0uMB8t9fBfrsSKf1p7LPvrHmrU06EH59MxgUuCX2zw/s72-c/lme4eg.png" height="72" width="72"/><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-7120858890579029286</id><published>2013-02-26T19:47:00.001-08:00</published><updated>2013-02-26T20:33:57.406-08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Heckman Selection model"/><category scheme="http://www.blogger.com/atom/ns#" term="PROC LIFEREG"/><category scheme="http://www.blogger.com/atom/ns#" term="PROC QLIM"/><category scheme="http://www.blogger.com/atom/ns#" term="Tobit Model"/><title type='text'>Poor man&#39;s HPQLIM? </title><content type='html'>Tobit model is a type of censored regression and is one of the most important regression models you will encounter in business. Amemiya 1984 classified Tobit models into 5 categories and interested reader can refer to SAS online doc for details. In SAS, PROC QLIM is used to estimate various types of Tobit models as well as related class of models, such as Heckman&#39;s selection model, etc.&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgPV7JAChf8MDE9QmUnYviu2vtUgvPi88R368_oNoKR9rRArCsECMYx4c-Rnd5EJZ7B5yNPoyyh4dtJXlTT9pzienI6yztKYDkIS7IIR3gOyk7x0yfQxGbmF22v0L3i13fhZZFL9w/s1600/Tobit.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;150&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgPV7JAChf8MDE9QmUnYviu2vtUgvPi88R368_oNoKR9rRArCsECMYx4c-Rnd5EJZ7B5yNPoyyh4dtJXlTT9pzienI6yztKYDkIS7IIR3gOyk7x0yfQxGbmF22v0L3i13fhZZFL9w/s400/Tobit.jpg&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
Jason&amp;nbsp;(Blog @ &lt;a href=&quot;http://analytics-in-writing.blogspot.com/&quot; target=&quot;_blank&quot;&gt;Analytics-in-writing&lt;/a&gt;)&amp;nbsp;in his recent &lt;a href=&quot;http://analytics-in-writing.blogspot.com/2013/02/tobit-and-limited-dependant-variable.html&quot; target=&quot;_blank&quot;&gt;post&lt;/a&gt; showed that PROC QLIM is not designed for large data, meaning for data set with millions or more records, and SAS developed HPQLIM, the High Performance version of QLIM, to deal with large scale modeling within Tobit model framework. While I celebrate the roll out of HPQLIM, I think the example Jason used is poorly chosen. He used standard Tobit model (type 1), while he really should use a more complicated one to demonstrate the full power of HPQLIM because there already is a solution in SAS to fit standard Tobit model on very large data set very fast.&lt;br /&gt;
&lt;br /&gt;
Before we dive into the example Jason mentioned, it is useful to review the concept of censoring and truncation in regression analysis. Suppose there is a variable Y following a continuous distribution D and a constant K. After a value y is sampled from Y:&lt;br /&gt;
&lt;br /&gt;
if y &amp;lt; K (y &amp;gt;&amp;nbsp;&lt;k y=&quot;&quot;&gt;&lt;k y=&quot;&quot;&gt;K) and &lt;b&gt;discard&lt;/b&gt; the whole record, then it is left (right) truncation;&lt;/k&gt;&lt;/k&gt;&lt;br /&gt;
&lt;br /&gt;
if y &amp;lt; K (y &amp;gt;&amp;nbsp;&lt;k y=&quot;&quot;&gt;&lt;k y=&quot;&quot;&gt;K) and &lt;b&gt;report&lt;/b&gt; y=K, then it is left (right) censoring.&lt;/k&gt;&lt;/k&gt;&lt;br /&gt;
&lt;br /&gt;
Jason showed that, when facing millions of records, PROC QLIM is very slow in estimating even the Type 1 Tobit model, the simplest left censoring at 0, and suggested to use HPQLIM which shows significant improvement in computing time. Unfortunately, not every business can afford High Performance (HPx) solution from SAS but Tobit model is so prevailing in observational study, people may wonder is there a way to get around this within the SAS environment but with much improved speed?&lt;br /&gt;
&lt;br /&gt;
Yes, at least for Type 1 Tobit model. SAS Manual provide an exampling using PROC LIFEREG for Type 1 Tobit model since it is left censoring and the log likelihood function is no difference from a parametric survival model with corresponding distribution:&lt;br /&gt;
&lt;br /&gt;
$$ l = \sum_{i \in (y=0)} \left [ 1- \Phi(X_i\beta/\sigma)\right ] + \sum_{i \in (y&amp;gt;0)} \left [ \Phi(X_i\beta/\sigma)/\sigma \right ] $$&lt;br /&gt;
&lt;br /&gt;
PROC LIFEREG is significantly faster comparing to PROC QLIM as it is optimized for this particular type of model. In order to demonstrate the speed gain, I used a similar data as Jason used, the training sample from KDD CUP 1998. This is the original data where the sample Jason used is from, with 95K observations and 400+ variables. In consistent with Jason&#39;s test, we will use 10 randomly selected numerical variables in the model fitting, and stack the data 100 times so that the final data feed to PROC QLIM &amp;amp; PROC LIFEREG has about 9.5 million observations.&lt;br /&gt;
&lt;br /&gt;
The SAS log below shows that, on a laptop with i5 2520M CPU clocked at 2.5Ghz and with 4G memory, PROC QLIM used almost 39 minutes while PROC LIFEREG only used less than 37 sec, more than 60 times difference. In comparison, HPQLIM in Jason&#39;s example uses 1/3 of the time of QLIM. Based on the real time data, Jason&#39;s OC (old client) machine runs about the same speed as my laptop using QLIM, so I expect the same results from HPQLIM if I could have run one, namely, 12 minutes real time, 27 minutes CPU time. Still not comparable to LIFEREG&#39;s less than 1 minute time usage.&lt;br /&gt;
&lt;br /&gt;
6092&lt;br /&gt;
&lt;br /&gt;
6093 %let keepvars=target_: age wealth1 hit CARDPROM numprom cardpm12 numprm12&lt;br /&gt;
&lt;br /&gt;
6094 RAMNTALL NGIFTALL CARDGIFT MINRAMNT LASTGIFT&lt;br /&gt;
&lt;br /&gt;
6095 ;&lt;br /&gt;
&lt;br /&gt;
6096 data pva;&lt;br /&gt;
&lt;br /&gt;
6097 set train(keep=&amp;amp;keepvars);&lt;br /&gt;
&lt;br /&gt;
6098 do i=1 to 100;&lt;br /&gt;
&lt;br /&gt;
6099 output;&lt;br /&gt;
&lt;br /&gt;
6100 end;&lt;br /&gt;
&lt;br /&gt;
6101 run;&lt;br /&gt;
&lt;br /&gt;
NOTE: There were 95412 observations read from the data set WORK.TRAIN.&lt;br /&gt;
&lt;br /&gt;
NOTE: The data set WORK.PVA has 9541200 observations and 15 variables.&lt;br /&gt;
&lt;br /&gt;
NOTE: DATA statement used (Total process time):&lt;br /&gt;
&lt;br /&gt;
real time 1:11.55&lt;br /&gt;
&lt;br /&gt;
cpu time 5.26 seconds&lt;br /&gt;
6102&lt;br /&gt;
&lt;br /&gt;
6103 proc qlim data=pva;&lt;br /&gt;
&lt;br /&gt;
6104 model target_d = age wealth1 hit CARDPROM numprom cardpm12 numprm12&lt;br /&gt;
&lt;br /&gt;
6105 RAMNTALL NGIFTALL CARDGIFT MINRAMNT LASTGIFT ;&lt;br /&gt;
&lt;br /&gt;
6106 endogenous target_d~censored(lb=0);&lt;br /&gt;
&lt;br /&gt;
6107 run;&lt;br /&gt;
NOTE: Convergence criterion (FCONV=2.220446E-16) satisfied.&lt;br /&gt;
&lt;br /&gt;
NOTE: At least one element of the gradient is greater than 1e-3.&lt;br /&gt;
&lt;br /&gt;
NOTE: PROCEDURE QLIM used (Total process time):&lt;br /&gt;
&lt;br /&gt;
real time 39:51.24&lt;br /&gt;
&lt;br /&gt;
cpu time 39:33.01&lt;br /&gt;
&lt;br /&gt;
6108&lt;br /&gt;
&lt;br /&gt;
6109 /* make it suitable for parametric survival model */&lt;br /&gt;
&lt;br /&gt;
6110 data pva;&lt;br /&gt;
&lt;br /&gt;
6111 set pva;&lt;br /&gt;
&lt;br /&gt;
6112 if target_d=. then y=0; else y=target_d;&lt;br /&gt;
&lt;br /&gt;
6113 run;&lt;br /&gt;
&lt;br /&gt;
NOTE: There were 9541200 observations read from the data set WORK.PVA.&lt;br /&gt;
&lt;br /&gt;
NOTE: The data set WORK.PVA has 9541200 observations and 16 variables.&lt;br /&gt;
&lt;br /&gt;
NOTE: DATA statement used (Total process time):&lt;br /&gt;
&lt;br /&gt;
real time 1:57.75&lt;br /&gt;
&lt;br /&gt;
cpu time 11.82 seconds&lt;br /&gt;
&lt;br /&gt;
6114&lt;br /&gt;
&lt;br /&gt;
6115 proc lifereg data=pva;&lt;br /&gt;
&lt;br /&gt;
6116 model (target_d, y) = age wealth1 hit CARDPROM numprom cardpm12 numprm12&lt;br /&gt;
&lt;br /&gt;
6117 RAMNTALL NGIFTALL CARDGIFT MINRAMNT LASTGIFT ;&lt;br /&gt;
&lt;br /&gt;
6118 run;&lt;br /&gt;
&lt;br /&gt;
NOTE: Algorithm converged.&lt;br /&gt;
&lt;br /&gt;
NOTE: PROCEDURE LIFEREG used (Total process time):&lt;br /&gt;
&lt;br /&gt;
real time 36.53 seconds&lt;br /&gt;
&lt;br /&gt;
cpu time 13.06 seconds&lt;br /&gt;
&lt;br /&gt;
One additional advantage of PROC LIFEREG is that it also can very easily handle observation-by-observation censoring, which is interval censoring in a survival analysis.&lt;br /&gt;
&lt;br /&gt;
The disadvantage of PROC LIFEREG comparing to PROC QLIM in the Tobit model framework is that PROC LIFEREG only computes predicted value for &lt;strong&gt;uncensored&lt;/strong&gt; observations, but in Tobit model, the expected value of &lt;strong&gt;censored&lt;/strong&gt; observations is of particular interests. To do this, analyst will have to use DATA STEP to manually compute this value for each of censored observations. But this is easy.&lt;br /&gt;
&lt;br /&gt;
For example, standard Tobit model usually assumes normal distribution for latent variable, and in this case, the expected observed value can be calculated as:&amp;nbsp;$$ E(Y_i) = \Phi(X_i\beta/\sigma)(X_i\beta + \sigma \lambda)$$&lt;br /&gt;
&lt;br /&gt;
where&amp;nbsp; $$ \sigma=SCALE $$&amp;nbsp;is from estimation and &amp;nbsp;$$\lambda=\phi(X_i\beta/\sigma)/\Phi(X_i\beta/\sigma)$$, where&amp;nbsp;$$\phi(), \Phi()$$ indicate density function and cumulative probability function from standard normal distribution, respectively.&lt;br /&gt;
&lt;br /&gt;
Alternatively, if a logistic regression is assumed, then $$ E(Y_i) = \sigma \log(1 + exp(X_i\beta/\sigma))$$&lt;br /&gt;
&lt;br /&gt;
Even if with this extra step to calculate necessary quantities, the total time it would take is still significantly smaller than using PROC QLIM. As seen from the log, go through the whole data set computing these quantities with 9.5 million records took less than 2 minutes, so the total time using PROC LIFEREG is less than 3 minutes, that is still only 8% of what it takes by PROC QLIM. Recall that in Jason&#39;s poset, HPQLIM runs on a Greenplum Application with 35 working nods still takes 1 minutes 24 seconds to finish the fitting and scoring process on a data set with 7.7 million records.&lt;br /&gt;
&lt;br /&gt;
Lastly but not the least, for this particular example, a Heckman&#39;s selection model is more appropriate because the intention to donate (the latent variable) is correlated with the amount of donation if the subject decides to donate, while a standard Tobit model literally assumes independence. And applying a Full Information Maximum Likelihood (FIML) estimation of Heckman&#39;s selection model on such a large data set is where HPQLIM really shine and there is no immediate replacement available in current SAS solution.&lt;br /&gt;
&lt;br /&gt;
Reference:&lt;br /&gt;
SAS Institute, Inc., SAS/ETS User&#39;s Guide for v9.2, Cary, NC&lt;br /&gt;
SAS Institute, Inc., SAS/STAT User&#39;s Guide for v9.2, Cary, NC.&lt;br /&gt;
&lt;br /&gt;
Amemiya, T. (1984), &quot;Tobit Models: A Survey,&quot; &lt;i class=&quot;itshape&quot;&gt;Journal of 
Econometrics&lt;/i&gt;, 24, 3–61.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;!--1--&gt;&lt;!--1--&gt;&lt;!--1--&gt;&lt;!--1--&gt;&lt;!--1--&gt;&lt;!--1--&gt;&lt;!--1--&gt;</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/7120858890579029286/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/7120858890579029286' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/7120858890579029286'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/7120858890579029286'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2013/02/poor-mans-hpqlim.html' title='Poor man&#39;s HPQLIM? '/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgPV7JAChf8MDE9QmUnYviu2vtUgvPi88R368_oNoKR9rRArCsECMYx4c-Rnd5EJZ7B5yNPoyyh4dtJXlTT9pzienI6yztKYDkIS7IIR3gOyk7x0yfQxGbmF22v0L3i13fhZZFL9w/s72-c/Tobit.jpg" height="72" width="72"/><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-6438567868248121297</id><published>2012-12-10T18:58:00.003-08:00</published><updated>2013-02-12T21:11:12.667-08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Data Mining"/><category scheme="http://www.blogger.com/atom/ns#" term="KNN"/><category scheme="http://www.blogger.com/atom/ns#" term="predictive modeling"/><category scheme="http://www.blogger.com/atom/ns#" term="PROC DISCRIM"/><category scheme="http://www.blogger.com/atom/ns#" term="SVD"/><title type='text'>Kaggle Digit Recoginizer: SAS k-Nearest Neighbor solution</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYjJbKj4ERPuH0H69Y4IRRcgUP0No7s1CvalYje4l20WWsRhZMtHuqdqfIDpzefsVFf-xyQCWHI8I0MBOy2qEjEkfgvBhf5cdFMSOhHm9IBpJTOEopVfSTWREWZKnEUcSqgM8NZg/s1600/kaggle+logo.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;148&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYjJbKj4ERPuH0H69Y4IRRcgUP0No7s1CvalYje4l20WWsRhZMtHuqdqfIDpzefsVFf-xyQCWHI8I0MBOy2qEjEkfgvBhf5cdFMSOhHm9IBpJTOEopVfSTWREWZKnEUcSqgM8NZg/s320/kaggle+logo.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;a href=&quot;http://www.kaggle.com/&quot; target=&quot;_blank&quot;&gt;Kaggle&lt;/a&gt; is hosting an educational data mining competition: &lt;a href=&quot;http://www.kaggle.com/c/digit-recognizer&quot;&gt;Kaggle Digit Recognizer&lt;/a&gt;, using MNIST data. Handwritten digit recognition is one of the few applications that kNN classifier performs well. Of course, the benchmark kNN classifier provided by the competition organizer is not even close to being sophisticated. So I decided to try the kNN Classifier in SAS using PROC DISCRIM. For details about how to do kNN classifier in SAS, see &lt;a href=&quot;http://www.sas-programming.com/search/label/KNN&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;&amp;nbsp;and &lt;a href=&quot;http://www.sas-programming.com/2012/11/knn-classification-and-regression-in-sas.html&quot; target=&quot;_blank&quot;&gt;here &lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
The data is pre-processed from raw images using NIST standardization program, but it&amp;nbsp;noteworthy some extra efforts to conduct more exploratory data analysis (EDA). Because this is a pre-processed data, therefore no missing data issues and EDA will focus on descriptive statistics of input variables. The code below read in the data which is stored in folder &quot;D:\DATA\MNIST&quot;;&lt;br /&gt;
&lt;br /&gt;
&lt;code&gt;
%let folder=D:\Data\MNIST;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;libname digits &quot;&amp;amp;folder&quot;;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;data digits.MNISTtrain;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;infile &quot;&amp;amp;folder.\train.csv&quot;  lrecl=32567  dlm=&#39;,&#39; firstobs=2;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;input  label x1-x784;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;run;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;data digits.MNISTtest;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;infile &quot;&amp;amp;folder.\test.csv&quot;  lrecl=32567  dlm=&#39;,&#39; firstobs=2;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;input  x1-x784;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;run;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;br /&gt;
Figure 1 below shows the distribution of variances of input variables in the Training set. Apparently there are many input variables of little information and they should be removed before further data processing and actual modeling. Next we examine Coefficient of Variation (CoV) of input variables. CoV gives a more consistent way to compare across variables. The result is demonstrated in figure 2. Note that variables with 0 variance will have their CoV be set to 0.&lt;br /&gt;
&lt;br /&gt;
&lt;code&gt;
ods select none;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;proc means data=digits.MNISTtrain ;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var x1-x784;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;output out=stat(where=(_STAT_ in (&#39;STD&#39;, &#39;MEAN&#39;)));&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;run;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;ods select all;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;proc transpose data=stat  out=stat_t;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;var _STAT_ x:;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;run;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;data stat_t;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;set stat_t;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;ID=_n_-1;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;if col2=0 then CoV=0; else  CoV=col1/col2;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;run;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;proc sgplot data=stat_t;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;scatter y=col2 x=ID;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;label ID=&quot;X #&quot;  col2=&quot;StdErr&quot;;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;run;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;proc sgplot data=stat_t;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;scatter y=CoV x=ID;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;label ID=&quot;X #&quot;;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;run;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhv0BuCO1GxfEURkok8p_X8V6RgWQthigG3T-qonmFfHdMz86yH0AqHSpmsF4JXRbWwdF50LFeWRustXuCFUjZtrSE5ttQ9aJrh8MdNBSvyf2Yfg5TazweMhDCtVct6B2J3TqBTsA/s1600/StdPlot.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;300&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhv0BuCO1GxfEURkok8p_X8V6RgWQthigG3T-qonmFfHdMz86yH0AqHSpmsF4JXRbWwdF50LFeWRustXuCFUjZtrSE5ttQ9aJrh8MdNBSvyf2Yfg5TazweMhDCtVct6B2J3TqBTsA/s400/StdPlot.png&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Figure 1. StdErr by input features&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhXyLD2JsQVjQlMLWZkHlJugoXllNTBf5fxDDBvupU5U_MX-OM-mlIcRnb-MwtkyqAjwCQWV2c5Hf6uDR9pSd0YlmSy34SkQte_WDAOf77q_oVmQCeE6_PtVOw5J95llfC6zaLYqA/s1600/CovPlot.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;300&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhXyLD2JsQVjQlMLWZkHlJugoXllNTBf5fxDDBvupU5U_MX-OM-mlIcRnb-MwtkyqAjwCQWV2c5Hf6uDR9pSd0YlmSy34SkQte_WDAOf77q_oVmQCeE6_PtVOw5J95llfC6zaLYqA/s400/CovPlot.png&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Figure 2. Coefficient of Variation by input features&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;br /&gt;
In this demonstration, we select features that have a variance larger than 10% of quantiles. Even so, we still end up with more than 700 features. Note that in SAS, the computing time of kNN classification increases in superlinear and kNN itself suffers curse of dimensionality, therefore it is desirable to conduct dimension reduction first before applying kNN classifiers. While there are so many different techniques to choose from, we decided to go with SVD, which is both widely used and simple to implement in SAS, see &lt;a href=&quot;http://www.sas-programming.com/2010/03/macro-for-svd.html&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt; for sample code and examples. The code below applies SVD to the data that combining both training and testing. This is important because SVD is sensitive to input data and include both data sets in the computing won&#39;t violate any best practice rules in predictive model because no information from dependent variable is used. The left eigenvectors are also standardized to have 0 mean and unit standard deviation.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
data all;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;set digits.MNISTtrain(in=_1)&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt;&amp;nbsp; &amp;nbsp; digits.MNISTtest(in=_2);&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;if _1 then group=&#39;T&#39;;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; else group=&#39;S&#39;;&lt;br /&gt;
run;&lt;br /&gt;
proc princomp data=all out=allpca(drop=x:) &amp;nbsp;outstat=stat noint cov noprint;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var x1-x784;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
proc standard data=allpca &amp;nbsp;out=allpcastd mean=0 &amp;nbsp;std=1;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;var Prin:;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
data allpcastd;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;set allpcastd;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;array _p{*} p1-p10 (10*0.1);&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt;&amp;nbsp;cv=rantbl(97897, of _p[*]);&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt;&amp;nbsp;drop p1-p10;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
After the data is compressed, we split them into 10 folds of subsets, flag each of them and search for best tuning parameter. In building a simple kNN classifier, there are two tuning parameters to decide, 1.what features to use and 2. the number of neighbors to use. In this exercise, we simplify the first problem into searching for the number of features, always starting from feature #1. We then calculate the error rate for each combination. Before we start a brutal force search, we have to understand that search through the whole space of combination of 700 features and 1 to 25 neighbors is not practical, it just takes too much time, may run for over 2 days on a regular PC. But we can narrow down to a range of number of features by testing number of features at every 10th interval, say test 1 feature, 11 features, 21 features,..., 201 features. In classification, kNN is more sensitive to features used instead of number of neighbors. The code below executes such experiment and will take about 18 hours to finish on my PC equipped with i5-3570 CPU, clocked at 3.8Ghz. In actual execution, I used my proprietary &lt;b&gt;%foreach MACRO&lt;/b&gt; that distribute the job to 3 cores of the CPU, and shaved the total running time to only 6 hours.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
/*@@@@@@@@@@ USE 10-FOLD CV @@@@@@@@@@*/&lt;br /&gt;
&lt;br /&gt;
%macro loopy;&lt;br /&gt;
%do k=2 %to 8;&lt;br /&gt;
&amp;nbsp; &amp;nbsp;%do i=1 %to 10;&lt;br /&gt;
&amp;nbsp; &amp;nbsp;%let t0=%sysfunc(time());&lt;br /&gt;
options nosource nonotes;&lt;br /&gt;
proc discrim data=allpcastd(where=(group=&#39;T&#39; &amp;amp; cv^=&amp;amp;i)) &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; method=npar &amp;nbsp;k=&amp;amp;k noprint&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;testdata=allpcastd(where=(group=&#39;T&#39; &amp;amp; cv=&amp;amp;i)) &amp;nbsp; &lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; testout=out(drop=Prin:)&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; class &amp;nbsp; label;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; var &amp;nbsp; &amp;nbsp; Prin1-Prin30;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
proc freq data=out noprint;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;table label*_INTO_/out=table;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
proc sql;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;create table _tmp as&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;select &amp;amp;i as cv, &amp;amp;k as K, 31 as dim, sum(PERCENT) as correct&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; from &amp;nbsp; table&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; where &amp;nbsp;label=_INTO_&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; ;&lt;br /&gt;
quit;&lt;br /&gt;
&amp;nbsp; &amp;nbsp;%let t1=%sysfunc(time());&lt;br /&gt;
&amp;nbsp; &amp;nbsp;data _null_;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; dt=round(&amp;amp;t1-&amp;amp;t0, 0.1);&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;  &lt;/span&gt;call symput(&#39;dt&#39;, dt);&lt;br /&gt;
&amp;nbsp; &amp;nbsp;run;&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp; &amp;nbsp;%put k=&amp;amp;k &amp;nbsp;i=&amp;amp;i time=&amp;amp;dt;&lt;br /&gt;
%if %eval(&amp;amp;k+&amp;amp;i)=3 %then %do;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; data result; set _tmp; run;&lt;br /&gt;
%end;&lt;br /&gt;
%else %do;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; data result; set result &amp;nbsp;_tmp; run;&lt;br /&gt;
%end;&lt;br /&gt;
%end;&lt;br /&gt;
%end;&lt;br /&gt;
options source notes;&lt;br /&gt;
%mend;&lt;br /&gt;
options nosource nonotes;&lt;br /&gt;
%loopy;&lt;br /&gt;
options source notes;&lt;br /&gt;
&lt;br /&gt;
proc means data=result noprint;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; by k;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp;var &amp;nbsp;correct;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp;output out=result_summary &amp;nbsp;mean(correct)=mean &amp;nbsp;std(correct)=std;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
&lt;div&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjqdJXTX-MBsbxww6PforvkNOtNLeEut9dR_hEJK9zWZwbGfL-ZfKdVpPORK3GGvBYmLwwNpGdmmPRhlHZkPcg8AcejpXP02bDXHVnQygfF2RDM5aUOPCTRPuBu0vjKRueGosUJbw/s1600/correctPlot.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;300&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjqdJXTX-MBsbxww6PforvkNOtNLeEut9dR_hEJK9zWZwbGfL-ZfKdVpPORK3GGvBYmLwwNpGdmmPRhlHZkPcg8AcejpXP02bDXHVnQygfF2RDM5aUOPCTRPuBu0vjKRueGosUJbw/s400/correctPlot.png&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Figure 3. Correct% vs. Number of Features by Number of Neighbors&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;br /&gt;
Now we see that the best possible number of features may exist somewhere between 21 to 41 while the number of neighbors has a much smaller influence except for using only 2 NN. Now we are in a good situation to conduct more greedy search over the feature space. We are going to search through all combinations of number of features from 21 to 41 at single interval and neighbors 3 to 7, which results in a total number of 120 iterations each runs over the 10 folds, and the code ran about 3 hours on my PC. The code below executes this exercise and the result is summarized in table 1. We can see that using up to 31 features and 3 nearest neighbors will be the best choice for the data at hand.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
%macro loopy;&lt;br /&gt;
%do k=2 %to 7;&lt;br /&gt;
%do d=21 %to 41;&lt;br /&gt;
&amp;nbsp; &amp;nbsp;%do i=1 %to 5;&lt;br /&gt;
&amp;nbsp; &amp;nbsp;%let t0=%sysfunc(time());&lt;br /&gt;
options nosource nonotes;&lt;br /&gt;
proc discrim data=allpcastd(where=(group=&#39;T&#39; &amp;amp; cv^=&amp;amp;i ))&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;method=npar &amp;nbsp;k=&amp;amp;k &amp;nbsp; noprint&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; testdata=allpcastd(where=(group=&#39;T&#39;)) &amp;nbsp; &lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;                       &lt;/span&gt;&amp;nbsp;testout=out(drop=Prin:)&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; class &amp;nbsp; label;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; var &amp;nbsp; &amp;nbsp; Prin1-Prin&amp;amp;d;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
proc freq data=out noprint;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;table label*_INTO_/out=table;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
proc sql;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;create table _tmp as&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;select &amp;amp;i as cv, &amp;amp;k as K, &amp;amp;d as dim, sum(PERCENT) as correct&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; from &amp;nbsp; table&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; where &amp;nbsp;label=_INTO_&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; ;&lt;br /&gt;
quit;&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp; &amp;nbsp;%let t1=%sysfunc(time());&lt;br /&gt;
&amp;nbsp; &amp;nbsp;data _null_;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; dt=round(&amp;amp;t1-&amp;amp;t0, 0.1);&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;  &lt;/span&gt;call symput(&#39;dt&#39;, dt);&lt;br /&gt;
&amp;nbsp; &amp;nbsp;run;&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp; &amp;nbsp;%put k=&amp;amp;k &amp;nbsp;d=&amp;amp;d &amp;nbsp;i=&amp;amp;i time=&amp;amp;dt;&lt;br /&gt;
%if %eval(&amp;amp;k=2) &amp;amp; %eval(&amp;amp;d=21) %then %do;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; data result; set _tmp; run;&lt;br /&gt;
%end;&lt;br /&gt;
%else %do;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; data result; set result &amp;nbsp;_tmp; run;&lt;br /&gt;
%end;&lt;br /&gt;
%end;&lt;br /&gt;
%end;&lt;br /&gt;
%end;&lt;br /&gt;
options source notes;&lt;br /&gt;
%mend;&lt;br /&gt;
options nosource nonotes;&lt;br /&gt;
%loopy;&lt;br /&gt;
options source notes;&lt;br /&gt;
&lt;br /&gt;
proc means data=result noprint nway;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; class &amp;nbsp;k dim;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp;var &amp;nbsp;correct;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp;output out=result_summary2 &amp;nbsp;mean(correct)=mean &amp;nbsp;std(correct)=std;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
proc means data=result_summary2 nway noprint;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;class k;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; var &amp;nbsp; mean;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; output &amp;nbsp;out=result_max &amp;nbsp;idgroup(max(mean) out[1](mean std &amp;nbsp;dim)=mean std &amp;nbsp;dim);&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
proc sgplot data=result ;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; where i=.;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp;dot dim /response=correct stat=mean limitstat=stddev numstd=1 &amp;nbsp;group=k;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp;label dim=&quot;# of Features&quot;;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgagTJAN9cyZw86Gdiv_2C9qYQNTme7Hli_zJ9yQpAl4jXc2uYPzfd6qLE6Bj7Qb_4VZSXqHmIRj_c0VO0hYCUuQGyZgkwMpafeyAVeaovHxZswc781znrbU2ifor46MR3vk_wNiQ/s1600/correctPlot_local.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;300&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgagTJAN9cyZw86Gdiv_2C9qYQNTme7Hli_zJ9yQpAl4jXc2uYPzfd6qLE6Bj7Qb_4VZSXqHmIRj_c0VO0hYCUuQGyZgkwMpafeyAVeaovHxZswc781znrbU2ifor46MR3vk_wNiQ/s400/correctPlot_local.png&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Figure 4. Correct% and Confidence Interval by # of features and # of neighbors.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;table border=&quot;0&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; style=&quot;border-collapse: collapse; text-align: center; width: 256px;&quot;&gt;&lt;colgroup&gt;&lt;col span=&quot;4&quot; style=&quot;width: 48pt;&quot; width=&quot;64&quot;&gt;&lt;/col&gt;&lt;/colgroup&gt;&lt;tbody&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;clear: right; margin-bottom: 1em; margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgF29XV6RdV9XmdFnUg_2hVZ2F8pa7Cf6rQKz0aHzPw9Du9IVtJlBC1on2_4t4gCXg8mToPOTOiuCAN_1snnSEWr4Hm1nU7Xs63vmCktIkU2oo0JndwnNDKRegUL6alb-aZjU1rSA/s1600/Table1.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;215&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgF29XV6RdV9XmdFnUg_2hVZ2F8pa7Cf6rQKz0aHzPw9Du9IVtJlBC1on2_4t4gCXg8mToPOTOiuCAN_1snnSEWr4Hm1nU7Xs63vmCktIkU2oo0JndwnNDKRegUL6alb-aZjU1rSA/s400/Table1.png&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Table 1. Tuning Parameters Selection&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;br /&gt;
&lt;br /&gt;
Now, re-score the data using chosen tuning parameters: up to 33 features and 3-NN, we end up an accuracy of &lt;b&gt;&lt;span style=&quot;color: red;&quot;&gt;96.80%&lt;/span&gt;&lt;/b&gt; on the&amp;nbsp;leader-board, which certainly is not the best result you can get from kNN method, but easily beat both the 1NN and ramdomForest benchmark results.&lt;br /&gt;
&lt;div&gt;
&lt;br /&gt;
With a little bit more tweak, the accuracy from a kNN with similar parameters can be boosted to 97.7%.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;!--6--&gt;&lt;br /&gt;
&lt;code&gt;
&lt;code&gt;
&lt;/code&gt;&lt;/code&gt;&lt;!--6--&gt;&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;/div&gt;
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/6438567868248121297/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/6438567868248121297' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/6438567868248121297'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/6438567868248121297'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2012/12/kaggle-digit-recoginizer-sas-k-nearest.html' title='Kaggle Digit Recoginizer: SAS k-Nearest Neighbor solution'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYjJbKj4ERPuH0H69Y4IRRcgUP0No7s1CvalYje4l20WWsRhZMtHuqdqfIDpzefsVFf-xyQCWHI8I0MBOy2qEjEkfgvBhf5cdFMSOhHm9IBpJTOEopVfSTWREWZKnEUcSqgM8NZg/s72-c/kaggle+logo.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-5950858322777821869</id><published>2012-11-25T13:47:00.004-08:00</published><updated>2012-11-28T09:13:22.561-08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Data Mining"/><category scheme="http://www.blogger.com/atom/ns#" term="KNN"/><category scheme="http://www.blogger.com/atom/ns#" term="Nearest Neighbor"/><category scheme="http://www.blogger.com/atom/ns#" term="PCA"/><category scheme="http://www.blogger.com/atom/ns#" term="predictive modeling"/><category scheme="http://www.blogger.com/atom/ns#" term="PROC DISCRIM"/><category scheme="http://www.blogger.com/atom/ns#" term="PROC KRIGE2D"/><category scheme="http://www.blogger.com/atom/ns#" term="SVD"/><title type='text'>KNN Classification and Regression in SAS</title><content type='html'>PDF available at &lt;a href=&quot;http://www.nesug.org/Proceedings/nesug12/sa/sa01.pdf&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;. Related post on KNN classification using SAS is &lt;a href=&quot;http://www.sas-programming.com/2010/05/k-nearest-neighbor-in-sas.html&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
In data mining and predictive modeling, it refers to a memory-based (or instance-based) algorithm for classification and regression problems. It is a widely used algorithm with many successfully applications in medical research, business applications, etc. In fact, according to Google Analytics, it is the second most viewed article on my SAS programming blog of all time, with more than 2400 views&amp;nbsp;in past&amp;nbsp;year. &lt;br /&gt;
&lt;br /&gt;
In classification problems, the label of potential objects is determined by the labels of closest training data points in the feature space. The determination process is either through &quot;majority voting&quot; or &quot;averaging&quot;, potentially with weighting. In &quot;majority voting&quot;, the label of object is assigned to be the label which most frequent among the k closest training examples. In &quot;averaging&quot;, the object is not assigned a label, but instead, the ratio of each class among the k closest training data points. In Regression problems, the property of object is obtained via a similar &quot;averaging&quot; process, where the value of the object is the average value of the k closest training points. &lt;br /&gt;
&lt;br /&gt;
In practice, both the &quot;majority voting&quot; and &quot;averaging&quot; process can be refined by adding weights to the k closest training points, where the weights are proportional to the distance between object and the training point. In this way, the closest points will have the biggest influence on the final results. &lt;br /&gt;
&lt;br /&gt;
In fact, the SAS implementation of kNN classification has the averaging process be weighted by volume size. Interested readers are encouraged to read the manual for details. kNN algorithm has several characteristics that worth addressing. Understanding its advantages and disadvantages in theory and in practice will help practicioners better leverage this tool. Generally, KNN algorithm has 4 advantages worth noting. It is simple to implement. &lt;br /&gt;
&lt;br /&gt;
Theoretically, kNN algorithm is very simple to implement. The naive version of the algorithm is easy to implement. For every data point in the test sample, directly computing the desired distances to all stored vectors, and choose those shortest k examples among stored vectors. It is, however, computationally intensive, especially when the size of the training set grows. Over the years, many nearest neighbor search algorithms have been proposed, seeking to reduce the number of distance evaluations actually performed. Using an appropriate nearest neighbor search algorithm makes k-NN computationally feasible even for large data sets. &lt;br /&gt;
&lt;br /&gt;
In SAS, the k-d Tree data structure and associated search algorithm of Friedman et. al. is implemented. It is analytically tractable. The error rate for 1-NN classification is twice as much as Bayesian Error in asymptotic sense. It is highly adaptive to local information. kNN algorithm uses the closest data points for estimation, therefore it is able to take full advantage of local information and form highly nonlinear, highly adaptive decision boundaries for each data point. It is easily implemented in parallel. Because it is instance based, for each data point to be scored, the algorithm check against the training table for the k nearest neighbor. Since each data point is independent of the others, the execution of search and score can be conducted in parallel. &lt;br /&gt;
&lt;br /&gt;
On the other hand, kNN algorithm has 3 disadvantages, which will also be addressed in details in section PRACTICAL ISSUES ON IMPLEMENTATION. It is computationally very intensive, because for each records to be classified and/or scored, the program has to scan through available data points to find the nearest neighbors and then determine which ones to use for further calculation. It is demanding in storage, because all records in training sample have to be stored and used whenever scoring is necessary. It is highly susceptible to curse of dimensionality, because as dimension increases, the distance calculation and finding the appropriate space to contain at least one relevant record is becoming difficult. &lt;br /&gt;
&lt;br /&gt;
In order to conduct either KNN Classification or KNN regression in SAS, there are two basic approaches. The first one utilize special options in certain SAS Procedures and conduct the KNN analysis directly. I call it &quot;Direct Approach&quot;; while the second method will first find the nearest data points explicitly leveraging search capability in some SAS procedures and manually calculate the final response values, which is simple anyway. I call it &quot;Manual Approach&quot;. To take the manual approach, there are three PROCs that are capable to find nearest neighbor points in efficient way, namely &lt;code&gt;PROC FASTCLUS, PROC KRIGE2D&lt;/code&gt; and &lt;code&gt;PROC MODECLUS&lt;/code&gt;. Each has its pro and con and all need data preparation. The discussion of this issue will beyond the scope of this paper. To take the direct approach, &lt;code&gt;PROC DISCRIM&lt;/code&gt; can be directly used for KNN Classification while &lt;code&gt;PROC KRIGE2D&lt;/code&gt; can be directly used for KNN regression, even though the functionality SAS provides is only basic. For kNN-based classification, &lt;code&gt;PROC DISCRIM&lt;/code&gt; offers intuitive programming interface and rich options. For kNN-based regression, even though there is not dedicated procedure available, &lt;code&gt;PROC KRIGE2D&lt;/code&gt; can be used to fulfill this task, with some tweaks. In this section, we will demonstrate how to conduct these two data mining tasks in SAS and address some closely related issues. For kNN-based regression, we will discuss the details of how to tweak &lt;code&gt;PROC KRIGE2D&lt;/code&gt;. SAS offers kNN classification capability via&amp;nbsp; &lt;code&gt;PROC DISCRIM&lt;/code&gt;, by invoking the following procedure options:&lt;br /&gt;
&lt;code&gt; METHOD=NPAR K= &lt;/code&gt;&lt;br /&gt;
&lt;br /&gt;
The &lt;code&gt;METHOD=NPAR&lt;/code&gt; option asks SAS to use non-parametric discrimination function, together with &lt;code&gt;K=&#39;&lt;/code&gt; option &lt;code&gt;PROC DISCRIM&lt;/code&gt; will use kNN classification, where &lt;code&gt;K=&lt;/code&gt; tells SAS how many neighbors to use in determining the . A typical SAS program for kNN-based classification looks like the follows: &lt;br /&gt;
&lt;br /&gt;
&lt;code&gt;&lt;br /&gt;&lt;/code&gt;
&amp;nbsp;proc discrim data&amp;nbsp;&amp;nbsp;&amp;nbsp; = train ................................[1]&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; method&amp;nbsp; = npar k=5 .............................[2]&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; test&amp;nbsp;&amp;nbsp;&amp;nbsp; = toscore ..............................[3]&amp;nbsp;&amp;nbsp; &lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; testout = toscore_out...........................[4]&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ; &lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; class y; ...............................................[5]&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; var x1-x10; ............................................[6]&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
We explain the function of each statement below. &lt;br /&gt;
&lt;br /&gt;
Statement [1] tells SAS that \verb|PROC DISCRIM| should be called to process the data named \emph{train}. &lt;br /&gt;
&lt;br /&gt;
Statement [2] tells SAS to apply kNN Classification method using 5 nearest neighbors. &lt;br /&gt;
&lt;br /&gt;
Statement [3] tells SAS that the classification rule be applied to a test data called &#39;toscore&#39;. &lt;br /&gt;
&lt;br /&gt;
Statement [4] tells SAS to output classification result for the test data and to name the output data as \emph{toscoreout}. &lt;br /&gt;
&lt;br /&gt;
Statement [5] defines Y as the dependent variable to use in \verb|PROC DISCRIM|. &lt;br /&gt;
&lt;br /&gt;
Statement [6] tells SAS to use x1 to x10 as input features in calculating the distance to neighbors. &lt;br /&gt;
&lt;br /&gt;
kNN classification is computationally intensive. Based on SAS manual [4], for the specific tree search algorithm implemented in \verb|PROC DISCRIM|, the time usage, excluding I/O time, is roughly proportional to $$\log(N)*(N*P)$$, where N is the number of observations and P is the number of variables used. kNN is a memory-based method, when an analyst wants to score the test data or new data in production, the whole raw table will be loaded to memory in the scoring process. In computing, \verb|PROC DISCRIM| uses memory approximately proportional to the second order of number of variables, i.e. proportional to $$P^2$$. Therefore, it would be interesting to see how the cost of computing, in terms of memory usage and time consumption, increases as the number of observations in training data and the number of features are increasing. \subsection{kNN Regression using SAS} kNN technique can be applied to regression problems, too, but the coding in SAS is not as straightforward as in a classification problem. kNN regression uses the average value of dependent variable over the selected nearest neighbors to generate predicted value for scoring data point. In SAS, we can use \verb|PROC KRIGE2D| to conduct this analysis, but it needs more cautions and tweaks. The following example code instructs SAS to do a kNN regression on variable Y using 5 nearest neighbors and 2 features: X1 and X2. &lt;br /&gt;
&lt;br /&gt;
&lt;code&gt; 
proc krige2d data=train outest=pred outn=NNlist; ..............[1]&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;coordinate xc=X1 yc=X2; ..........................................[2]&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;predict var=Y NP=5; .................................................[3]&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;model scale=10 range=1E-7 form=exp; ......................[4]&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;grid griddata=toscore xc=X1 yc=X2; ...........................[5]&amp;nbsp;&lt;/code&gt;&lt;br /&gt;
&lt;code&gt;&amp;nbsp;run; 
&lt;/code&gt; 
&lt;br /&gt;
&lt;code&gt;&lt;br /&gt;&lt;/code&gt;
Statement [1] invokes \verb|KRIGE2D| procedure and ask SAS to operate on data named \emph{train}, and output the prediction to data named \emph{pred} via option OUTEST=. The option OUTN= ask SAS to output information about the selected nearest neighbors of each data points in the data specified in GRID statement. KRIGE2D will directly output the prediction of dependent variable in the data from &#39;OUTEST= &#39;, but only output the corresponding value of dependent variable in the selected closest data points as well as the coordinates. This is a disadvantage in the sense that when cross validation is conducted, the data from &#39;OUTN=&#39; does not contain the actual value of dependent variable and this data set has to be merged back to the validation part to obtain actual values so that errors can be computed. &lt;br /&gt;
&lt;br /&gt;
Statement [2] tells \verb|KRIGE2D| that the names of the 2 coordinates to be used, or in the language of data mining, the 2 features to be used. In this example, it is X1 and X2. &lt;br /&gt;
&lt;br /&gt;
Statement [3] tells \texttt{KRIGE2D} to use 5 nearest neighbors to predicts dependent variables&#39;s value at current grid position. As to be discussed, the number of nearest neighbors is a tuning parameter in this algorithm, and should be determined using data driven approaches, such as Cross Validation. &lt;br /&gt;
&lt;br /&gt;
Statement [4] tells \texttt{KRIGE2D} what model to be used in kriging using &#39;FORM=&#39; option and the associated parameters, scale and range. Note that, in order to conduct a kNN regression, it is required to specify a large scale value and a small range value. For example, in example above, scale is set to 10 while range is set to 1E-7. Typically, a scale value larger than 1 and a range value smaller than 1E-7 work well. The type of model is irrelevant. The reason why this works is beyond the scope of this paper and we will not discuss on this issue here. &lt;br /&gt;
&lt;br /&gt;
Statement [5] tells \texttt{KRIGE2D} that the name of the data to be scored is \emph{tosore} in option \texttt{GRIDDATA=} and the corresponding names of features (coordinates) to be used for scoring. &lt;br /&gt;
&lt;br /&gt;
\verb|PROC KRIGE2D| was designed to perform ordinary kriging in two dimensions, therefore it is not fully functional in the standard kNN regression practice. There are two major issues with \verb|PROC KRIGE2D| when it is applied in a kNN regression analysis.&lt;br /&gt;
&lt;br /&gt;
The first problem is that \verb|PROC KRIGE2D| accept two and only two inputs as features because it treat kNN regression as a two dimensional kriging. The second problem is due to the nature of semivariogram model used in kriging. In cases where the data are dense and highly overlapped, \verb|PROC KRIGE2D| will report singularity in kriging system. However, these issues should not bother analysis in practice and there are good reasons which will be detailed below.&lt;br /&gt;
&lt;br /&gt;
Overcome Fixed Number of Features in KRIGE2D:&lt;br /&gt;
&lt;br /&gt;
In a real kNN regression application, the number of features ranges from 1 to hundreds if necessary, but \verb|PROC KRIGE2D| requires exactly two features as inputs. When only 1 feature is used, or when more than 2 features are required, the analysts can take a detour by pre-processing the data up front. We will discuss these two cases separately below.&lt;br /&gt;
&lt;br /&gt;
Only one feature. When only one feature is used, we can set up a dummy input, which is a constant number, in both the training data and scoring data. The dummy variable will cancel out in calculating, thus it has no effect virtually. The code below shows a demonstration:&lt;br /&gt;
&lt;br /&gt;
&lt;code&gt;&lt;/code&gt;&lt;br /&gt;
data trainv/view=trainv;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;set train;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;dummy=1;&lt;br /&gt;
run;&lt;br /&gt;
data toscorev/view=toscorev;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; set toscore;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; dummy=1;&lt;br /&gt;
run;&lt;br /&gt;
proc krige2d data=trainv outest=pred outn=knn;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;coordinate xc=dummy yc=feature1;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;predict var=Y NP=5;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;model scale=10 range=1E-7 form=exp;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; grid gdata=toscorev xc=dummy yc=feature1;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
In the example code above, we used SAS data view to avoid re-write the data sets and save computing resources. This technique is explained in \cite{view}.&lt;br /&gt;
&lt;br /&gt;
More than two features. When more than two features are used, there are two approaches to handle this problem. 

The first approach conduct dimension reduction upfront and \verb|PROC KRIGE2D| take two derived new dimensions as features to perform kNN regression; the second approach follows the ensemble principle \cite{ESL}, and randomly select two features out of the pool of available features. The final kNN regression prediction is an average of predictions from each of the ensemble regressions. The first approach can also use the ensemble principle, too, in practice. There are many dimension reduction techniques, and among all, the most widely used is Singular Value Decomposition (SVD), or its equivalent counterpart Principal Components Analysis (PCA). Details of SVD or PCA is beyond the scope of this paper and interested readers can refer to \cite{PCA}, \cite{Matrix}.&lt;br /&gt;
&lt;br /&gt;
The example code in Appendix 1 demonstrates the first method where we conduct dimension reduction using PCA upfront and use the leading two orthogonal bases for kNN regression. Note that in the example code, we first combine both train and score data sets together because PCA is sample sensitive \cite{PCA}, and using records from both training data and scoring data will stabilize the generated orthogonal bases.&lt;br /&gt;
&lt;br /&gt;
The second method is demonstrated below. It is worth noting that when the number of features is small, say 10, it is a good practice to select all pairwise combination as input features and ensemble all results at the final stage. On the other hand, when the number of such combinations blows as the number of features increases, a random sample of pairwise combination works fine, just like in a RandomForest algorithm. In the code below, we assume 100 features which are stored in a table called FEATURES and the ensemble iterates 50 times, each with a pair of randomly selected features.&lt;br /&gt;
&lt;br /&gt;
&lt;code&gt;&lt;br /&gt;
%macro knnreg(train_dsn, score_dsn, feature1 , feature2 , loopid);&lt;br /&gt;
&amp;nbsp; &amp;nbsp; proc krige2d data=&amp;amp;train_dsn outest=pred&amp;amp;loopid outn=nn&amp;amp;loopid;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; coord xc=&amp;amp;feature1 yc=&amp;amp;feature2;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; pred var=Y NP=10;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; model scale=2 range=1E-8 form=exp;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; grid gdata=&amp;amp;score_dsn xc=&amp;amp;feature1 yc=&amp;amp;feature2;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; run;&lt;br /&gt;
%mend;&lt;br /&gt;
%let nloops=50;&lt;br /&gt;
proc surveyselect data=features out=feature2 reps=&amp;amp;nloops sampsize=2 method=srs;&lt;br /&gt;
run;&lt;br /&gt;
data _null_;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; set feature2;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; by replicate;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; retain train_dsn &#39;train&#39; score_dsn &#39;toscore&#39;;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; retain _id 0; array _f{2} $ _temporary_;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if first.replicate then do;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;call missing(of _f[*]); &lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;_id=1;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; end;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; _f[_id]=varname; _id+1;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;if last.replicate then do;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; call execute(&#39;%knnreg(&#39; || compress(train_dsn) || &#39;,&#39;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; || compress(score_dsn) || &#39;,&#39;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; || compress(_f[1]) || &#39;,&#39;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; || compress(_f[2]) || &#39;)&#39; );&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;end;&lt;br /&gt;
run;&lt;br /&gt;
&lt;/code&gt;&lt;br /&gt;
&lt;br /&gt;
The &#39;&lt;i&gt;call execute&lt;/i&gt;&#39; statement calls the pre-defined macro \%kddreg within a data step. For details about &#39;&lt;i&gt;call execute&lt;/i&gt;&#39; command, consult \cite{CallExecute}.&lt;br /&gt;
&lt;br /&gt;
&lt;b&gt;&lt;i&gt;Singularity in kriging process&lt;/i&gt;&lt;/b&gt;.&amp;nbsp;When the scoring point is extremely close to at least one of the training record, local ordinary kriging process will be singular and the estimation for the scoring point will be skipped. One solution is to set a large number for scale and small number for range in MODEL statement, for example:&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; MODEL SCALE=10 RANGE=1e-8 FORM=EXP;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
In the extreme cases, while inconvenient, it is necessary to manually process the data containing nearest neighboring points from &lt;code&gt;OUTN= &lt;/code&gt; to generate prediction. It turns out to be fairly simple, because ordinary kNN regression only requires average every k observations. This technique is demonstrated in Appendix 2. In such case, it is unnecessary to output predictions from KRIGE2D directly, and the output can be suppressed by specifying &lt;code&gt;OUTEST=_NULL_&lt;/code&gt;&lt;br /&gt;
&lt;br /&gt;
&lt;b&gt;&lt;i&gt;Practical issues on implementation&lt;/i&gt;&lt;/b&gt;. &amp;nbsp;kNN is a proven technique in many classification and regression problems. To fully appreciate its power in real analysis, there are a couple of issues need to be addressed appropriately. These problems touch the pain points of efficiency, accuracy, robustness and more. In the sections below, we will first discuss efficiency around storage and computation issues and see how parallelization can improve the efficiency. Then we will discuss model selection that balances accuracy and robustness. Lastly, we will discuss other miscellaneous issues when applying KNN in practice.&lt;br /&gt;
&lt;br /&gt;
&lt;i&gt;Optimize Storage and Computation&lt;/i&gt;. kNN algorithm is expensive in both storage and computation. Therefore, it is important to the factors that contribute to complexity in time and space of this algorithm, thus optimize the implementation in SAS.&lt;br /&gt;
&lt;br /&gt;
Optimize Storage. kNN classification requires a lot of storage because this is a in-memory algorithm, and all the training information to score a new data will be load into the memory to build a kd-Tree when conduct scoring. Therefore, when the size of the training data is large, and when the number of variables used is large, the required memory storage will be non-trivial. According to SAS/STAT Manual, the required storage for a kNN classification is at least &lt;code&gt;c(32v+3l+128)+8v^2+104v+4l &lt;/code&gt;; bytes, where v is the number of variables, c is the number of classes levels of the dependent variable, and l is the length of CLASS variable. Apparently, the storage requirement is linear in the latter two factors which are given by the problem at hand, while quadratic in the number of variables. To optimize storage, it is necessary to reduce the number of variables involved in the analysis.&lt;br /&gt;
&lt;br /&gt;
1. Only choose the necessary variables in kNN analysis. In many cases, 2 or 3 features is enough, actually. On the other hand, like in RandomForest algorithm, multiple runs of kNN algorithm were conducted, each with a small subset of variables, and these classifiers or regression results will be ensembeled to be the final solution.&lt;br /&gt;
&lt;br /&gt;
2. When selected number of variables is still relatively large, one commonly used technique for dimension reduction is Singular Value Decomposition (SVD), or equivalently, the Principal Component Analysis (PCA). Because kNN algorithm is highly adaptive to local information, SVD or PCA won&#39;t suffer from common critics that the information in dependent variable is not used when variable reduction is used. This technique is demonstrated in the Digits Recognition example below.&lt;br /&gt;
&lt;br /&gt;
kNN algorithm used in classification and regression suffers a lot from curse of dimensionality, therefore, choosing only necessary and more useful features for the problem in hand is critical for the success of application and implementation.&lt;br /&gt;
&lt;br /&gt;
Optimize Computation. kNN classification is also computationally intensive. Unlike many classification methods, such as logistic regression, discriminant analysis and other parametric regressions, kNN has no condense mathematical form to represent the information in training data, and lack of explicit model training. When scoring is needed, the training data is simply used to populate the sample search space with known results, and scoring is no different from a searching and comparison process, which is very time consuming. In the most naive way of searching, the algorithm compares scoring current data point to all data in the training sample, calculate desired distance metrics and select the top k nearest neighbors for their response information. This is computationally very redundant and intensive and doesn&#39;t leverage the distribution information in training sample well. Modern algorithm relies on heuristic searching algorithms and one of the most significant one is kd-Tree \cite{kd-tree}. kd-Tree stands for k-dimensional tree, and it is a space-partitioning data structure to organize data points in k-dimensional space for efficient search. In SAS, &lt;code&gt;PROC DISCRIM &lt;/code&gt;; stores data in kd-Tree structure. The time required to organize the observations into this specific tree structure is proportional to $$vn\ln(n)$$. The time for performing each tree search is proportional to $$\ln(n)$$. Apparently, the way to reduce computational time is reduce the number of data points n. On the other hand, it is advised to keep enough data points in training sample. This is because a k-d trees are not suitable for efficiently finding the nearest neighbor in high dimensional spaces. As a general rule, if the dimensionality is k, the number of points in the data, N, should be N &amp;gt;&amp;gt; 2k. Otherwise, when k-d trees are used with high-dimensional data, most of the points in the tree will be evaluated and the efficiency is no better than exhaustive search, and approximate nearest-neighbor methods are used instead \cite{SAS-STAT}. \subsection{Select the Optimal Values for Tuning Parameters} kNN algorithms have two basic tuning parameters, the number of nearest neighbors k, and the dimension of feature space d. The typical way to find the right values for these two parameters is brutal force computation, where program searches through a grid of different combinations of these two parameters and decide the best set based on cross validation results. The k in kNN refers to the number of closest data points, which is an important tuning parameter in kNN algorithm. There is no set rule for choosing the right k value, but usually when the data is dense, k is set to be small while when data is sparse, a relatively higher k should be used to reduce the variance. In many cases, the dimension is pre-determined. However, kNN is sensitive to curse of dimensionality and more often than not, the features are derived values, such as Principle Component Scores. The analysts have to find which subset of features is enough for the problem at hand. According to Friedman, et al, an objective data driven approach to choose the right value for k is the way to go. Typically, a Cross-Validation (CV) based approach is used, usually 5-fold CV or 10-fold CV. When using an M-fold CV to choose the tuning parameter k and d, it is customized to check the mean value and standard deviation of certain model accuracy measurement. For example, for Classification problem, both error rate and AUC can be used; for regression problems, RMSE is a good candidate measurement. The code in the two examples below illustrate these techniques.&lt;br /&gt;
&lt;br /&gt;
&lt;i&gt;Parallelization&lt;/i&gt;. kNN classification and regression can be easily parallelized. The most significant applications are in Cross-validation based tuning parameter evaluation and scoring. In Cross-Validation process, the analyst is able to open M concurrent sessions, each overs mutually exclusive set of tuning parameters. For example, the simplest case is issuing 2 concurrent SAS sessions, one examines &lt;code&gt;k=1, ..., 5&lt;/code&gt; while the other one examines &lt;code&gt;k=6, ... , 11&lt;/code&gt;. The actual implementation follows the idea of \cite{MultiThreading}. In scoring, because each data point is assumed to be independent from the others, the scoring program can split the scoring data set into several pieces, depending on the number of available CPUs and the master control module will simultaneously call the scoring module which only operates on each piece at a time.&lt;br /&gt;
&lt;br /&gt;
&lt;b&gt;&lt;i&gt;kNN in Action&lt;/i&gt;&lt;/b&gt;. In this section, we demonstrate kNN algorithms in real applications. Both examples will go through steps such as data introduction, data analysis and modeling using kNN algorithm, as well as selecting the tuning parameter k. The first example examines handwritten digits recognition, a 10-label classification problem. The second example examines a regression problem where the researcher wanted to estimate the phenolic records.&lt;br /&gt;
&lt;br /&gt;
&lt;b&gt;&lt;i&gt;kNN Classification in Optical Digits&lt;/i&gt;&lt;/b&gt;. One successful application of kNN classifiers is in digits recognition projects, see \cite{LeCun1998} for examples. In this example, I will demonstrate a complete data analysis process using kNN classifier to identity hand written digits from Optical Digits images. The data is from the pre-processed Optical Digits images available at UC Irving Machine Learning data set repository and can be downloaded at&amp;nbsp;&lt;a href=&quot;http://archive.ics.uci.edu/ml/datasets/Optical+Recognition+of+Handwritten+Digits&quot; target=&quot;_blank&quot;&gt;the link here&lt;/a&gt;&amp;nbsp;.&lt;br /&gt;
&lt;br /&gt;
The data is extracted using the NIST preprocessing routines \cite{NIST} from normalized bitmaps of handwritten digits from a preprinted form. The original bitmaps are 32X32 but divided into non-overlapping blocks of 4X4 and the the number of on pixels are counted in each block. This generates an input matrix of 8x8 where each element is an integer in the range 0..16. This reduces dimensionality to 64 instead of 1024, and gives invariance to small distortions. In this project, the following steps are performed to fine tune the kNN classifier.&lt;br /&gt;
&lt;br /&gt;
First, the training and testing data in CSV format are imported at \emph{Step 1}.&lt;br /&gt;
&lt;br /&gt;
Then a baseline 1-NN classifier using all 64 features is built for comparison purpose at \emph{Step 2}. This baseline classifier return an error rate of 3.73\%.&lt;br /&gt;
&lt;br /&gt;
In \emph{Step 3}, a fine tuned kNN classifier is built, where the tuning parameter k, number of closest points, and d, number features, are selected based on 10-fold Cross Validation.&lt;br /&gt;
&lt;br /&gt;
Besides, instead of using the original 64 features, a Singular Value Decomposition (SVD) was conducted on the training and testing data sets combined to obtain derived features. This data pre-processing technique brings in several advantages.&lt;br /&gt;
&lt;br /&gt;
First, the derived features are ranked by how much variance in the feature space each explained, so that the selection of features becomes simpler using CV.&amp;nbsp;Second, SVD changes original sparse feature space into a dense one, and in general fewer closest data points are required to obtain optimal performance in this case.&lt;br /&gt;
&lt;br /&gt;
In \emph{Step 4}, the selected kNN classifier, with k=4 closest data points and d=36 derived features, is applied to the testing data set, obtaining an error rate of 1.89\%.&lt;br /&gt;
&lt;br /&gt;
As an alternate to 10-fold CV, in \emph{Step 5}, Leave-One-Out CV was used to fine tune the kNN classifier, resulting a set of parameters of k=6 and d=4.&lt;br /&gt;
&lt;br /&gt;
In \emph{Step 6}, the new classifier was applied to the testing data set, resulting in an error rate of 1.4\%. \end{enumerate} The 10-fold Cross Validation process is pretty computationally intensive even for such a small data set. Scanning across 900 different combinations of k, d pairs took almost 25 minutes on a main stream PC. The pay off worth it, however, as the error rate drops to 1.89\% from 3.73\%, a 49\% improvement. On the other hand, using Leave-One-Out Cross Validation process took 22 minutes on the same PC, and the selected parameter pair is k=6, d=38, with an error rate of 2.06\% from the same scoring data set, which is not as good as the result from 10-fold CV but still way better than baseline result.&lt;br /&gt;
&lt;br /&gt;
The 10-fold CV is preferred to Leave-One-Out cross validation from pure computational perspective, as the computing time for the latter one increases quadratically and will become infeasible when the number of observation is more than 100K. Figure \ref{fig:C1} shows the error rate as a function of k and d, while figure \ref{fig:C2} zooms in for a closer look at area where number of features is between 20 and 40 and closest data points is smaller than 5. We can see that globally, the error rate is a relatively smooth function of number of features over all and the number of neighbors does not influence the error rate as much as the number of features does, while locally there is significant variation as shown in figure \ref{fig:C2}. This implies that one heuristic way to scan for the best possible numbers of features is bi-section searching.&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjRhb5bgH2o9zzebB2Og01EVisKYO1qTMjxa143XPlgyJdctpqMB_XPQEFiDgB6jUHKLVFuymPUEfjFyKOgCfXX4_EopWUXyHuwxPukn_No7HU6Fgv65-H0WpgxUp_6Z53F1KqhTg/s1600/Class_Error_Rate_1.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;300&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjRhb5bgH2o9zzebB2Og01EVisKYO1qTMjxa143XPlgyJdctpqMB_XPQEFiDgB6jUHKLVFuymPUEfjFyKOgCfXX4_EopWUXyHuwxPukn_No7HU6Fgv65-H0WpgxUp_6Z53F1KqhTg/s400/Class_Error_Rate_1.png&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEilpRzGOUUDRhAiuNRYosuI-VcwPZEr24_DywRFWxlfpEVWXBixMtlEjUClfnn0j6TxSDctMUDV6_jPVoECQrbOFy5kpOpG48Vx4zkUgHo-kR7a0Tv_dEMlJW1mQ-c7cfb1k8jFVQ/s1600/Class_Error_Rate_2.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;300&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEilpRzGOUUDRhAiuNRYosuI-VcwPZEr24_DywRFWxlfpEVWXBixMtlEjUClfnn0j6TxSDctMUDV6_jPVoECQrbOFy5kpOpG48Vx4zkUgHo-kR7a0Tv_dEMlJW1mQ-c7cfb1k8jFVQ/s400/Class_Error_Rate_2.png&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
The complete SAS code that conducts brute force searching for tuning parameters can be found in Appendix 1. 

kNN Regression Example. As a demonstration of kNN regression, we used the \emph{Galaxy} data set from the book \emph{Elements of Statistical Learning 2}, which is available at \cite{galaxy}. The galaxy data was used to demonstrate smoothing techniques in the book and was visualized in figure 6.8, where local linear regression with nearest neighbor window of 15\% of the data in $\textrm{R}^2$ was applied to predict velocity data of galaxies.&lt;br /&gt;
&lt;br /&gt;
In our example, a k-Nearest Neighbor prediction is applied, where two values of k are used: k=5 and k=48. k=48 is equivalent to using 15\% of the data as neighbor while k=5 is arbitrarily chosen to demonstrate the fact that smaller number of neighbors will produce more accurate but higher variance prediction while larger value of k will product less accurate but smaller variance hence smoother prediction.&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZhVq7V2xCB_TrclrInOXfcIfnVeXcazJ76t-Nv5htMj6vOI1KGORWXh3nQGPrOAYaMK9me-a7wiZP-BaIkUq9DKdA85gMJu0_JXPogBwcN1PlAnXTy3qiDCGa_6pUoihS4aQQ_w/s1600/Combined.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;305&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZhVq7V2xCB_TrclrInOXfcIfnVeXcazJ76t-Nv5htMj6vOI1KGORWXh3nQGPrOAYaMK9me-a7wiZP-BaIkUq9DKdA85gMJu0_JXPogBwcN1PlAnXTy3qiDCGa_6pUoihS4aQQ_w/s400/Combined.png&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
Combine the two predictions in many cases may product better results among all. In figure \ref{fig:galaxy}, 3-D views of actual data and smoothed surface using different smoothing parameters are shown. The upper left panel shows the raw data. Obviously, the galaxy has a star like spatial shape with a total of about 10 arms extending outside from the center. The upper left panel shows smoothed 3-D surface using 5-nearest neighbors while the lower left panel shows smoothed 3-D surface using 48-nearest neighbors. It is noticeable that using less neighbors produce a rougher surface but adaptive better to local where there are more data, such as along the galaxy arms, but in more empty spaces, the prediction is wilder. On the other hand, using 48 neighbors produces much smoother surface yet the galaxy arms are less visible. The lower right panel shows the average of these two surface maps. This averaged out map keeps the galaxy arms clearly visible yet smooth out empty space pretty well.&lt;br /&gt;
&lt;br /&gt;
The complete SAS code is in Appendix 2. &lt;br /&gt;
&lt;br /&gt;
kNN algorithm is an old but very powerful technique if used appropriately. It has been shown that both kNN Classification and kNN regression can be carried out within SAS/STAT very easily. The functions are basic but more than enough to handle many real world problems. Both the theory and practice are discussed, with example SAS code demonstrated. The discussion here, however, is only the tip of iceberg. There are many advanced topics that can&#39;t be fitted into this short article. These topics include but not limited to: customized distance function, tangent invariance, etc. Interested readers are encouraged to read the related books and papers. &lt;br /&gt;
&lt;br /&gt;
&lt;b&gt;&lt;i&gt;Appendix 1.&lt;/i&gt;&lt;/b&gt;&lt;br /&gt;
&lt;code&gt;&lt;/code&gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
/*@@@@ --- Step 1 --- @@@@*/&lt;br /&gt;
libname digits &quot;C:\_Data\Digits&quot;;&lt;br /&gt;
&lt;br /&gt;
data digits.train;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;infile &quot;C:\_Data\Digits\optdigits.tra&quot; &amp;nbsp;lrecl=512 &amp;nbsp;dlm=&#39;,&#39;;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;input &amp;nbsp;x1-x64 y;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
data digits.test;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;infile &quot;C:\_Data\Digits\optdigits.tes&quot; &amp;nbsp;lrecl=512 &amp;nbsp;dlm=&#39;,&#39;;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; input &amp;nbsp;x1-x64 y;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
/*@@@@ --- Step 2 --- @@@@*/&lt;br /&gt;
proc discrim data=digits.train &amp;nbsp; testdata=digits.test&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; testout=testout&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; method=npar &amp;nbsp;k=1 noprint;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; class y;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; var &amp;nbsp; x1-x64;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
proc freq data=testout;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;table y*_INTO_;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
/*@@@@ --- Step 3 --- @@@@*/&lt;br /&gt;
data all;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;set digits.train(in=_1) &amp;nbsp; &amp;nbsp; digits.test(in=_2);&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;if _1 then Group=&#39;T&#39;;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;else Group=&#39;S&#39;;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
proc princomp data=all &amp;nbsp;out=pca noprint noint cov;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;var x1-x64;&lt;br /&gt;
run;&lt;br /&gt;
proc standard data=pca &amp;nbsp;out=pcaout &amp;nbsp;mean=0 &amp;nbsp;std=1;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;var Prin1-Prin62;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
/*@@@@@ 10-fold CV approach @@@@@@@@@@*/&lt;br /&gt;
data pcaout;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;set pcaout;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; array _xp{10} p1-p10 (10*0.1);&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; cv=rantbl(2342342, of _xp[*]);&lt;br /&gt;
run;&lt;br /&gt;
proc freq data=pcaout; &amp;nbsp; &amp;nbsp;table cv; &amp;nbsp;run;&lt;br /&gt;
&lt;br /&gt;
%macro loopy;&lt;br /&gt;
ods select none;&lt;br /&gt;
options nonotes nosource;&lt;br /&gt;
%do k=1 %to 15;&lt;br /&gt;
&amp;nbsp; &amp;nbsp;%do n=2 %to 60;&lt;br /&gt;
&amp;nbsp; &amp;nbsp;%put k=&amp;amp;k &amp;nbsp; n=&amp;amp;n;&lt;br /&gt;
&amp;nbsp; &amp;nbsp;%do cv=1 %to 10;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;proc discrim data=pcaout(where=(group=&#39;T&#39; &amp;amp; cv^=&amp;amp;cv))&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; testdata=pcaout(where=(group=&#39;T&#39; &amp;amp; cv=&amp;amp;cv))&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;testout=testout&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;method=npar &amp;nbsp;k=&amp;amp;k noprint;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;class y;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;var &amp;nbsp; Prin1 - Prin&amp;amp;n;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;run;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;proc freq data=testout;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; table y*_INTO_ /out=result;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;run;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;proc sql;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; create table _tmp&amp;amp;cv as&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;select sum(COUNT) as total,&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; sum((y=_INTO_)*COUNT) as correct,&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;amp;k as k, &amp;amp;n as n,&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 1-calculated correct / calculated total as error format percent7.3&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;from &amp;nbsp; result&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; &amp;nbsp; ;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;quit;&lt;br /&gt;
&amp;nbsp; %end;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt;data _tmp;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; &amp;nbsp;set %do cv=1 %to 10; &amp;nbsp; _tmp&amp;amp;cv &amp;nbsp; &amp;nbsp; &amp;nbsp;%end;;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt;run;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt;proc means data=_tmp noprint;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; &amp;nbsp; var &amp;nbsp;error;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;output &amp;nbsp;out=_tmp &amp;nbsp;mean(error)=error &amp;nbsp;std(error)=error_std;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt;run;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt;data _tmp;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; &amp;nbsp; length k n 8;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;set _tmp;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;k=&amp;amp;k; n=&amp;amp;n; &amp;nbsp;drop _TYPE_;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt;run;&lt;br /&gt;
&amp;nbsp; %if %eval(&amp;amp;k+&amp;amp;n=3) %then %do;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; data iter; set _tmp; run;&lt;br /&gt;
&amp;nbsp; %end;&lt;br /&gt;
&amp;nbsp; %else %do;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; proc append base=iter data=_tmp &amp;nbsp;force; run;&lt;br /&gt;
&amp;nbsp; &amp;nbsp;%end;&lt;br /&gt;
&lt;br /&gt;
%end;&lt;br /&gt;
%end;&lt;br /&gt;
ods select all; options notes source;&lt;br /&gt;
%mend;&lt;br /&gt;
&lt;br /&gt;
dm log &#39;clear&#39;;&lt;br /&gt;
%let time0=%sysfunc(time());&lt;br /&gt;
%loopy;&lt;br /&gt;
%let time1=%sysfunc(time());&lt;br /&gt;
&lt;br /&gt;
data _null_;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;dt=(&amp;amp;time1-&amp;amp;time0)/60;&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; put dt= &#39;Min&#39;;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
data iter;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;set iter;&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; format error percent7.4;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
proc transpose data=iter out=itert name=n prefix=n;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;by k;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;var error;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;id n;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
data itert;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;set itert;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;array _n{*} n2-n60;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;min=min(of _n[*]); &amp;nbsp;max=max(of _n[*]); &amp;nbsp;format min max percent7.4;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
/*@@@@ --- Step 4 --- @@@@*/&lt;br /&gt;
/*@@@@ use selected parameter to score the testing data @@@@*/&lt;br /&gt;
proc discrim data=pcaout(where=(group=&#39;T&#39;)) &amp;nbsp; testdata=pcaout(where=(group=&#39;S&#39;))&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; testout=testout(drop=x: Prin:)&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; method=npar &amp;nbsp;k=4 noprint;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; class y;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; var &amp;nbsp;Prin1-Prin36;&lt;br /&gt;
run;&lt;br /&gt;
proc freq data=testout;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;table y*_INTO_ /out=result;&lt;br /&gt;
run;&lt;br /&gt;
proc sql;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;create table temp as&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; select sum(COUNT) as total,&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;sum((y=_INTO_)*COUNT) as correct,&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp;4 as k, 36 as n,&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp;1-calculated correct / calculated total as error format percent7.3&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; from &amp;nbsp; result&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; ;&lt;br /&gt;
quit;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
/*@@@@ --- Step 5 --- @@@@*/&lt;br /&gt;
/*@@@@@ Use leave-one-out cross-validation approach @@@@@@*/&lt;br /&gt;
%macro loop;&lt;br /&gt;
%do k=1 %to 15;&lt;br /&gt;
%do i=1 %to 62;&lt;br /&gt;
proc discrim data=pcaout(where=(group=&#39;T&#39;)) &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; method=npar &amp;nbsp;k=&amp;amp;k &amp;nbsp; noprint &amp;nbsp;crossvalidate &amp;nbsp; &lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; outcross=outcross &amp;nbsp; out=out;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; class &amp;nbsp; y;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; var &amp;nbsp; &amp;nbsp; Prin1-Prin&amp;amp;i;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
proc freq data=outcross noprint;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;table y*_INTO_ /out=table;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
proc sql;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;create table _tmp as&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;select &amp;amp;k as K, &amp;amp;i as I, sum(PERCENT) as correct&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; from &amp;nbsp; table&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; where &amp;nbsp;y=_INTO_&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; ;&lt;br /&gt;
quit;&lt;br /&gt;
%if %eval(&amp;amp;i+&amp;amp;k)=2 %then %do;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; data result; set _tmp; run;&lt;br /&gt;
%end;&lt;br /&gt;
%else %do;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; proc append base=result data=_tmp; run;&lt;br /&gt;
%end;&lt;br /&gt;
%end;&lt;br /&gt;
%end;&lt;br /&gt;
%mend;&lt;br /&gt;
&lt;br /&gt;
options nonotes nosource;&lt;br /&gt;
%let time0=%sysfunc(time());&lt;br /&gt;
dm log &#39;clear&#39;;&lt;br /&gt;
%loop;&lt;br /&gt;
options notes source;&lt;br /&gt;
%let time1=%sysfunc(time());&lt;br /&gt;
data _null_;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;dt=(&amp;amp;time1-&amp;amp;time0)/60;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; put dt= &#39;Min&#39;;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
proc transpose data=result out=result_t name=i prefix=n;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;by k;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;var correct;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; id i;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
data result_t;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;set result_t;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; array _n{*} n2-n62;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; min=min(of _n[*]);&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; max=max(of _n[*]);&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; loc_min=whichN(min, of _n[*]); &amp;nbsp;loc_max=whichN(max, of _n[*]);&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; format min max best.;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
proc discrim data=pcaout(where=(group=&#39;T&#39;))&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;testdata=pcaout(where=(group=&#39;S&#39;))&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; testout=testout(drop=x: Prin:)&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt; method=npar &amp;nbsp;k=6 noprint;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; class y;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; var &amp;nbsp;Prin1-Prin38;&lt;br /&gt;
run;&lt;br /&gt;
proc freq data=testout;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;table y*_INTO_ /out=result;&lt;br /&gt;
run;&lt;br /&gt;
proc sql;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;create table temp as&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; select sum(COUNT) as total,&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;sum((y=_INTO_)*COUNT) as correct,&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt;6 as k, 38 as n,&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt;   &lt;/span&gt;1-calculated correct / calculated total as error format percent7.3&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; from &amp;nbsp; result&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; ;&lt;br /&gt;
quit;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;b&gt;&lt;i&gt;Appendix 2.&lt;/i&gt;&lt;/b&gt;&lt;br /&gt;
&lt;code&gt;&lt;/code&gt;&lt;br /&gt;
&lt;br /&gt;
proc import datafile=&quot;c:\galaxy.csv&quot; dbms=csv &amp;nbsp;out=galaxy replace; run;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
proc g3d data=galaxy;&lt;br /&gt;
&amp;nbsp; &amp;nbsp;scatter east_west*north_south=velocity /rotate=25 &amp;nbsp;size=1 noneedle &amp;nbsp;tilt=45;&lt;br /&gt;
&amp;nbsp; &amp;nbsp;format _numeric_ 8.2;&lt;br /&gt;
run;&lt;br /&gt;
quit;&lt;br /&gt;
&lt;br /&gt;
%macro knnreg(k);&lt;br /&gt;
ods select none;&lt;br /&gt;
proc krige2D data=galaxy &amp;nbsp;outest=knn_pred&amp;amp;k &amp;nbsp;outn=neighbor;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;coordinates xc=east_west &amp;nbsp;yc=north_south;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; predict &amp;nbsp; &amp;nbsp; var=velocity &amp;nbsp;NP=&amp;amp;k;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; model &amp;nbsp; &amp;nbsp; &amp;nbsp; scale=10 &amp;nbsp; &amp;nbsp; &amp;nbsp;range=1E-8 form=exp;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; grid &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;gdata=a(drop=velocity) &amp;nbsp; xc=east_west &amp;nbsp; yc=north_south ;&lt;br /&gt;
run;&lt;br /&gt;
ods select all;&lt;br /&gt;
&lt;br /&gt;
proc g3d data=knn_pred;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; plot gxc*gyc=estimate / &amp;nbsp;grid &amp;nbsp;rotate=25 tilt=45&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;ctop=cx993366&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;cbottom=cx9999ff&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;caxis=black;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; label &amp;nbsp; gxc=&quot;east_west&quot;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; gyc=&quot;north_south&quot;;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; format _numeric_ 8.2;&lt;br /&gt;
run;&lt;br /&gt;
&amp;nbsp; &lt;br /&gt;
%mend;&lt;br /&gt;
%knnreg(5);&lt;br /&gt;
%knnreg(48);&lt;br /&gt;
&lt;br /&gt;
data knn_pred;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp;merge knn_pred5(rename=(estimate=estimate5)) &lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; knn_pred48(keep=Estimate &amp;nbsp;rename=(estimate=estimate48));&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; estimate=(estimate5+estimate48)*0.5;&lt;br /&gt;
run;&lt;br /&gt;
&lt;br /&gt;
proc g3d data=knn_pred;&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; plot gxc*gyc=estimate / &amp;nbsp;grid &amp;nbsp;rotate=25 tilt=45&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;ctop=cx993366&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;cbottom=cx9999ff&lt;br /&gt;
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;caxis=black;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; label &amp;nbsp; gxc=&quot;east_west&quot;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; gyc=&quot;north_south&quot;;&lt;br /&gt;
&lt;span class=&quot;Apple-tab-span&quot; style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt; format _numeric_ 8.2;&lt;br /&gt;
run; &amp;nbsp; &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border-bottom: #999999 1px dashed; border-left: #999999 1px dashed; border-right: #999999 1px dashed; border-top: #999999 1px dashed; color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; width: 100%;&quot;&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/5950858322777821869/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/5950858322777821869' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/5950858322777821869'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/5950858322777821869'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2012/11/knn-classification-and-regression-in-sas.html' title='KNN Classification and Regression in SAS'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjRhb5bgH2o9zzebB2Og01EVisKYO1qTMjxa143XPlgyJdctpqMB_XPQEFiDgB6jUHKLVFuymPUEfjFyKOgCfXX4_EopWUXyHuwxPukn_No7HU6Fgv65-H0WpgxUp_6Z53F1KqhTg/s72-c/Class_Error_Rate_1.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-7983410932353094623</id><published>2012-10-04T08:24:00.000-07:00</published><updated>2012-11-18T14:23:30.100-08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Data Mining"/><category scheme="http://www.blogger.com/atom/ns#" term="Finite Mixture Model"/><category scheme="http://www.blogger.com/atom/ns#" term="LGD"/><category scheme="http://www.blogger.com/atom/ns#" term="Over-dispersion"/><category scheme="http://www.blogger.com/atom/ns#" term="predictive modeling"/><category scheme="http://www.blogger.com/atom/ns#" term="PROC FMM"/><category scheme="http://www.blogger.com/atom/ns#" term="RISK"/><title type='text'>Finite Mixture Model for Loss Given Default (LGD)</title><content type='html'>Loss Given Default (LGD) is a key business metric of risk in financial service. One unique feature of this metric is overdispersion and the other is multi-mode. &lt;br /&gt;
&lt;br /&gt;
Finite mixture model is an effective way to accommodate both. Multi-mode refers to the case where the distribution has more than one peak. Over-dispersion refers to the case where the dispersion of the data is more than what the assumed model implies. &lt;b&gt;&lt;u&gt;[3]&lt;/u&gt;&lt;/b&gt; provides a good introduction to finite mixture models. &lt;br /&gt;
&lt;br /&gt;
Consider again the loss given default case discussed in the paper of Matt Flynn &lt;b&gt;&lt;u&gt;[1].&lt;/u&gt;&lt;/b&gt; In this section, a complete modeling process is presented using finite mixture model that leverage the computing capability of PROC FMM in SAS v9.3 to model the variance in data that is not captured in a unified single model. Notice that because the dependent variable is bounded between 0 and 1, there are several options for modeling purpose. One can use a beta model, as discussed above; or a logit transformation can be applied first to the dependent variable (lgd) and a normal distribution is usually assumed to model the data. In this exhibition, the second approach is used. &lt;br /&gt;
&lt;br /&gt;
First of all, a complete Exploratory Data Analysis (EDA) is conducted to study the distribution and relationship between and among dependent variable and covariates. Here, EDA consists of two exercises. At step one, the distribution and pairwise relationship of variables are studied. Then, non-parametric models are applied to each covariate to study the possible functional forms between dependent variable and independent variables. &lt;br /&gt;
&lt;br /&gt;
The first figure shows the distribution of logit transformed Loss Given Default data. Apparently, it mimics normal distribution much closer than the raw distribution, but still it skews towards left rather than symmetric. Figure 2 below shows a matrix view of pairwise relationship between pairs of the four variables in the data &lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhUkd9nN2HQzi5NYypHWiiu1PtaduZcBGQ3UoKKFlTkcEx2APt3DyQ8NCANQmAk5vzd5ov8IAUJp05Zb0bbsSHTToBhehmzvxGKBirjTk7Q7LsKMmNZGjrOoKAuXOQa9FIM3jwfVQ/s1600/Figure1.PNG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;312&quot; mea=&quot;true&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhUkd9nN2HQzi5NYypHWiiu1PtaduZcBGQ3UoKKFlTkcEx2APt3DyQ8NCANQmAk5vzd5ov8IAUJp05Zb0bbsSHTToBhehmzvxGKBirjTk7Q7LsKMmNZGjrOoKAuXOQa9FIM3jwfVQ/s400/Figure1.PNG&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
Figure 1. Histogram of Logit Transformed Loss Given Default Variable&lt;/div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHCa7DKt8zvOnCJksqXIPxDnSb6QBQHWjs2H_p4jnZyUNl9erEQEXtb5qed6z7SDLj4p0I4ysZ7fvIgfCEoldGL1posY9T8-mhhjUg79Uq0N_SsiUU9U4W67t96AtuuAX3i40JpQ/s1600/Figure2.PNG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;400&quot; mea=&quot;true&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHCa7DKt8zvOnCJksqXIPxDnSb6QBQHWjs2H_p4jnZyUNl9erEQEXtb5qed6z7SDLj4p0I4ysZ7fvIgfCEoldGL1posY9T8-mhhjUg79Uq0N_SsiUU9U4W67t96AtuuAX3i40JpQ/s400/Figure2.PNG&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
Figure 2. Matrix Scatter Plot of All Variables.&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
In addition to a simple descriptive analysis using raw variables, it is often useful to conduct a non-parametric univariate regression so that the possible functional relationship between dependent variable and each covariate can be studied. Figure 3 shows the LOESS regression results of logit transformed LGD against the three covariates, namely, the annual average default rate, leverage coefficient by companies and industrial average default rate. It is noticeable that there is slightly quadratic relationship between logit(LGD) and average default rate by year and by industry. The linear relationship between logit(LGD) and leverage coefficient, however, is robust. On the other hand, we suspect that there is over-dispersion that can’t be captured adequately by these variables. &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;/div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi9DuhsXki_5uV5Agpk3Ez3N0Msc4DoHG5bL5qi49AeomBi_AvcJgb2jr1Mo8PbJO0s9NpJrFA2BxTBEb1-OPu6ySjds6P2jUkYW73dTNk_4ZBJG1-tBd2qB18ZfrvhDiPA0rp6Wg/s1600/Figure3.PNG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;194&quot; mea=&quot;true&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi9DuhsXki_5uV5Agpk3Ez3N0Msc4DoHG5bL5qi49AeomBi_AvcJgb2jr1Mo8PbJO0s9NpJrFA2BxTBEb1-OPu6ySjds6P2jUkYW73dTNk_4ZBJG1-tBd2qB18ZfrvhDiPA0rp6Wg/s400/Figure3.PNG&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
Figure 3. LOESS regression exhibition LGD against average default rate by year, leverage coefficient by firm and industrial average default rate.&lt;/div&gt;
&lt;br /&gt;
In order to examine the sample and their relationship closer, an OLS model is built with both average default rates by year and by firm are modeled using quadratic relationship and leverage coefficient by firm is modeled as a linear term.&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEio-grfAS3MZMtW7CPtnraBAzpjVLBfy89xOf-sdz-yQOToxX6EEMPkzaj68hX9kSfOuvHnv3vmtRTXqlNZZGexRmP1enymOPC1ky1-vR4G8MlgA1FTkhCrGXIEL7_1ssQPHtns7g/s1600/Figure4.PNG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;320&quot; mea=&quot;true&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEio-grfAS3MZMtW7CPtnraBAzpjVLBfy89xOf-sdz-yQOToxX6EEMPkzaj68hX9kSfOuvHnv3vmtRTXqlNZZGexRmP1enymOPC1ky1-vR4G8MlgA1FTkhCrGXIEL7_1ssQPHtns7g/s320/Figure4.PNG&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
Figure 4. Diagnostics from polynomial OLS&lt;/div&gt;
&lt;br /&gt;
Major model fitting diagnostic results are shown in figure 4. Key observations are:&lt;br /&gt;
&lt;br /&gt;
1. The fit is reasonably well, with residuals approximately normally distributed.&lt;br /&gt;
&lt;br /&gt;
2. There is noticeable over-dispersion when the residuals are plotted against predicted values. The variance is smaller at higher predicted values but larger at lower range.&lt;br /&gt;
&lt;br /&gt;
3. Q-Q plot also indicates existence of over-dispersion.&lt;br /&gt;
&lt;br /&gt;
As an alternative approach to a polynomial OLS, a finite mixture model is proposed. As a demonstration, we don’t go through the complete modeling process, especially the testing process to determine appropriate number of latent components; instead, a 2-component finite mixture model is used while only linear terms of the three covariates will enter the model. The purpose of this design is to show that even with a simpler and hence easier to interpret individual model structure, a finite mixture model is able to come up a better model that captures major stochastic effects in the data.&lt;br /&gt;
&lt;br /&gt;
The following code builds a 2-component mixture model and examines the distribution of residuals.&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border-bottom: #999999 1px dashed; border-left: #999999 1px dashed; border-right: #999999 1px dashed; border-top: #999999 1px dashed; color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; width: 100%;&quot;&gt;&lt;div align=&quot;left&quot;&gt;
ods html;
ods graphics on;
proc fmm data=lgddata2 plots(unpack);
     model lgd2 = lev lgd_a i_def /k=2 ;
     output out=fmm_out pred resid(component) resid(overall);
run;quit;
ods graphics off;
proc sgplot data=fmm_out;
     density resid_1 /type=kernel;
     density resid_2 /type=kernel;
     histogram resid ;
     density resid /type=normal;
run;
&lt;/div&gt;
&lt;/pre&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiCPOfEkfKCMcXKTSY31yacLfqrG1h4ykgAUKqDaJB_UmD7G36JhREELRZFggOLLnF6imoMnVY4rpO7NjXGk8dJaFtRtg65Fec18bBAU8uZj51KQbTrTL8tcYc1HcWaYN7Iphyphenhyphen6fA/s1600/Figure5.PNG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;300&quot; mea=&quot;true&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiCPOfEkfKCMcXKTSY31yacLfqrG1h4ykgAUKqDaJB_UmD7G36JhREELRZFggOLLnF6imoMnVY4rpO7NjXGk8dJaFtRtg65Fec18bBAU8uZj51KQbTrTL8tcYc1HcWaYN7Iphyphenhyphen6fA/s400/Figure5.PNG&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
Figure 5. Distribution of residuals from a 2-component normal mixture model&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
There are two key features from PROC FMM that worth mention. First, the mixture of distributions can be from different families. For example, a normal distribution can be mixed with a T-distribution. Second, the probability each data point belongs to a latent class can be modeled with covariates, so to enhance the model interpretability. &lt;br /&gt;
&lt;br /&gt;
The following code demonstrates a 2-component mixture model with one component is modeled as normally distributed while the other one is modeled as T-distributed. &lt;br /&gt;
&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border-bottom: #999999 1px dashed; border-left: #999999 1px dashed; border-right: #999999 1px dashed; border-top: #999999 1px dashed; color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; width: 100%;&quot;&gt;&lt;div align=&quot;left&quot;&gt;
/* mixture of heterogeneous distributions */&lt;br /&gt;
ods graphics on;&lt;br /&gt;
proc fmm data=lgddata2;&lt;br /&gt;
model lgd2 = lev lgd_a i_def /dist=normal;
     model      + lev lgd_a i_def /dist=t ;
     output out = fmm_out2 pred resid(component) resid(overall);
run;&lt;br /&gt;
ods graphics off;&lt;/div&gt;
&lt;/pre&gt;
The idea is simple. Outline a group of individual models of common distribution family in one MODEL statement, and the other groups of individual models of the same distribution family in other MODEL statement, without specifying the dependent variable but instead a ‘+’ symbol to indicate it is one layer add-up to existing individual models. Each group of individual models can be a mixture of multiple components. For example, in above code, if ‘K=2’ is specified in one of the MODEL statement, say in the first MODEL statement, then it tells SAS to model the data as a mixture of 2-component normal distribution and 1-component T distribution. This capability of modeling data with heterogeneous mixture distributions is a powerful tool in predictive modeling and advanced analytics. Of course, in this particular sample data, there is no immediate benefit by using a heterogeneous mixture distribution.&lt;br /&gt;
&lt;br /&gt;
The second feature that gives PROC FMM extreme power in advanced analytics is the capability to model probability model of latent classes using covariates, so that analysts are able to study which factors will contribute to classifying data points into different latent classes. This feature gives analyst the power to interpret latent class with insightful factors and even provide better model fit. What an analyst needs to do is to add the following statement:&lt;br /&gt;
&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border-bottom: #999999 1px dashed; border-left: #999999 1px dashed; border-right: #999999 1px dashed; border-top: #999999 1px dashed; color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; width: 100%;&quot;&gt;&lt;div align=&quot;center&quot;&gt;
PROBMODEL &amp;amp;covars;&lt;/div&gt;
&lt;/pre&gt;
&lt;br /&gt;
The probability model can be one of the following four: LOGITISTIC, PROBIT, LOG-LOG, and COMPLEMENTARY LOG-LOG. Please refer to SAS/STAT manual &lt;u&gt;&lt;b&gt;[2]&lt;/b&gt;&lt;/u&gt; for details.&lt;br /&gt;
&lt;br /&gt;
Reference:&lt;br /&gt;
[1]. Matt Flynn,&amp;nbsp;&lt;a href=&quot;http://www.casact.org/education/spring/2011/handouts/C11-Flynn.pdf&quot;&gt;http://www.casact.org/education/spring/2011/handouts/C11-Flynn.pdf&lt;/a&gt;&lt;br /&gt;
[2]. SAS Institute, Inc., SAS/STAT v9.3&amp;nbsp;User&#39;s Guide&lt;br /&gt;
[3]. Geoffrey McLachlan, David Peel, Finite Mixture Models, John Wiley &amp;amp; Sons, 2000 &lt;br /&gt;
Complete Code:
&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border-bottom: #999999 1px dashed; border-left: #999999 1px dashed; border-right: #999999 1px dashed; border-top: #999999 1px dashed; color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; width: 100%;&quot;&gt;&lt;div align=&quot;left&quot;&gt;
data lgddata;
   informat lgd  lev  12.9 lgd_a 6.4  i_def 4.3;
   input lgd lev lgd_a i_def;
   label lgd   = &#39;Real loss given default&#39;
      lev   = &#39;Leverage coefficient by firm&#39;
      lgd_a = &#39;Mean default rate by year&#39;
      i_def = &#39;Mean default rate by industry&#39;;
   cards;
0.747573451 0.413989786 0.6261 1.415
0.99 0.413989786 0.6849 1.415
0.06581075 0.230361142 0.4566 1.183
0.351287992 0.541339309 0.6261 2.353
0.25844921 0.541339309 0.4566 2.353
0.01968009 0.812 0.6715 0.743
0.931035513 0.546732229 0.6715 2.353
0.341254925 0.71 0.6715 1.183
0.35075456 0.855339361 0.6715 2.353
0.045826764 0.313983237 0.6261 0.743
0.025754193 0.190648237 0.4566 0
0.759732568 0.490953756 0.6261 2.353
0.757989999 0.910788759 0.6261 1.415
0.6 0.336071518 0.6261 1.183
0.374480256 0.414862374 0.4566 0.967
0.168726407 0.612063995 0.6261 1.183
0.283909643 0.693928717 0.6261 2.353
0.747018382 0.937072431 0.6261 1.415
0.686300059 0.801162532 0.6715 2.353
0.050051313 0.365725066 0.4566 0
/* More data is not shown here to save space */
;
run;

/* dep var is between (0, 1), using logit transformation */
data lgddata2;
      set lgddata;
   lgd2=log(lgd/(1-lgd));
run;

/* check raw dep var distribution */
proc sgplot data=lgddata2;
      histogram lgd2;
   density   lgd2/type=normal;
run;

/* check relationship between raw dep and covariates */
proc sgscatter data=lgddata2;
      matrix  lgd2  lev lgd_a  i_def;
run;


/* study the relationship */
ods graphics on;
proc loess data=lgddata2;
      model lgd2 = lev;
run;

proc loess data=lgddata2;
     model lgd2 = lgd_a;
run;

proc loess data=lgddata2;
      model lgd2 = i_def;
run;
ods graphics off;

data lgddata2;
      set lgddata2;
   lgd_a2=lgd_a**2;
   i_def2=i_def**2/10;
run;

/* build baseline model */
ods graphics on;
proc reg data=lgddata2;
      model lgd2 = lev lgd_a i_def  lgd_a2   i_def2;
   output  out=reg_out  pred  resid;
run;quit;
ods graphics off;


ods html;
ods graphics on;
proc fmm   data=lgddata2  plots(unpack);
      model lgd2 = lev  lgd_a  i_def /k=2 ;
   *probmodel lev  lgd_a  i_def;
   output  out=fmm_out  pred  resid(component)  resid(overall);
   *bayes;
run;quit;
ods graphics off;

proc sgplot data=fmm_out;
      density   resid_1 /type=kernel;
   density   resid_2 /type=kernel;
   *density   resid_3 /type=kernel;
   histogram   resid ;
   density     resid /type=normal;
run;


/* mixture of heterogeneous distributions */
ods graphics on;
proc fmm  data=lgddata2;
     model   lgd2 = lev  lgd_a  i_def  /dist=normal;
  model       + lev  lgd_a  i_def  /dist=t  ;
  output  out=fmm_out2  pred resid(component)  resid(overall);
  PROBMODEL &amp;amp;covars;
run;
ods graphics off;


proc sgplot data=fmm_out2;
      density   resid_1 /type=kernel;
   density   resid_2 /type=kernel;
   *density   resid_3 /type=kernel;
   histogram   resid ;
   density     resid /type=normal;
run;
&lt;/div&gt;
&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/7983410932353094623/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/7983410932353094623' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/7983410932353094623'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/7983410932353094623'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2012/10/finite-mixture-model-for-loss-given.html' title='Finite Mixture Model for Loss Given Default (LGD)'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhUkd9nN2HQzi5NYypHWiiu1PtaduZcBGQ3UoKKFlTkcEx2APt3DyQ8NCANQmAk5vzd5ov8IAUJp05Zb0bbsSHTToBhehmzvxGKBirjTk7Q7LsKMmNZGjrOoKAuXOQa9FIM3jwfVQ/s72-c/Figure1.PNG" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-1806703496617507180</id><published>2012-07-12T12:51:00.001-07:00</published><updated>2012-10-15T18:46:31.002-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Erlang C"/><category scheme="http://www.blogger.com/atom/ns#" term="PROC FCMP"/><category scheme="http://www.blogger.com/atom/ns#" term="Queueing Model"/><title type='text'>SAS functions for computing parameters in Erlang-C model</title><content type='html'>Call center management is both Arts and Sciences. While driving moral and setting up strategies is more about Arts, staffing and servicing level configuration based on call load is in the domain of Sciences.&lt;br /&gt;
&lt;br /&gt;
The science part of call center management is based on Queueing Theory, which studies &quot;the Phenomena of standing, waiting and serving&quot; [1], [2].The queueing model abstracts the complex phenomena of call center acticities into an idealized mathematical framework, so that it is possible to study the key characteristics and dynamics of the real world. There are many different models that could be applied to Call Center management, one simple yet popular model is called Erlang-C model. Some other highly related models are Erlang-B and Erlang-A models, but they won&#39;t be covered in this post.&lt;br /&gt;
&lt;br /&gt;
In this article, Erlang-C model will be briefly described and the computation of key parameters of this model will be discussed, followed by a complete suite of SAS code pieces. It is interesting that Erlang-C model is rarely touched in the SAS community. Searching through SAS-L archives, there were only two discussions relevant, and they are set apart by &lt;i&gt;&lt;b&gt;10 years &lt;/b&gt;&lt;/i&gt;[3], [4] ! The responders includes legendary David Cassell, SAS guru Daniel J. Nordlund, SAS computing expert &lt;span id=&quot;MSGHDR-REPLY-TO-PRE&quot;&gt;Rick Wicklin (&lt;a href=&quot;http://blogs.sas.com/content/iml/author/rickwicklin/&quot; target=&quot;_blank&quot;&gt;his SAS blog&lt;/a&gt;), and of course me.&lt;/span&gt;&lt;br /&gt;
&lt;br /&gt;
Erlang-C model is a commonly used mathematical framework for many Call center staffing and management softwares. Based on Kendall notation, this model can be expressed as M/M/C/\inf, where the first M indicates a Poisson distribution for Arrival process, while the second M indicates an Exponential distribution for servicing time. Both &#39;M&#39; refers markovian the &#39;C&#39; indicates number of servers which is a non-negative integer, while the last \inf refers infinite number of places in the system. Erlang-C system has extra assumption that it is First In First Out (FIFO) with infinite calling population. Therefore, Erlang-C model describe a Call Center that serves customers in the order they arrive in, while customers arrives following a Poisson process and the servicing time is Exponentially distributed. When a customer is not immediately served, it is put on hold until next available server (CSR). The customers are assumed to be patient so that they won&#39;t drop out but instead wait how-long-it-takes until next CSR. The system can take infinite number of waiting customers.&lt;br /&gt;
&lt;br /&gt;
In Erlang-C model, the traffic to a Call Center is measured in a so-called term of Offered Traffic Load, which is defined to be the ratio between the parameter \lambda of Poisson process and the parameter \mu of Exponential servicing time. Offered Traffic Load is therefore interpreted as the average number of services that are trying to happen simultaneously. It is free of time unit, but does has its own unit, called Erlangs. &lt;br /&gt;
&lt;br /&gt;
At the core of Erlang-C model is the Erlang-C function. The Erlang C function computes the probability that an arriving customer in the Erlang C queueing model will find that all servers are busy. This is the same as the fraction of arriving customers that are delayed (i.e., must wait) before beginning service. &lt;br /&gt;
&lt;br /&gt;
Erlang-C function is expressed as:&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; P_c(n, x) = dPoisson(n, x)/(dPoisson(n, x) - (1-x/n)*pPoisson(n, x))&lt;br /&gt;
&lt;br /&gt;
where dPoisson(n, x) is the density function of Poisson distribution with support 0,1, .., n and shape parameter x. pPoisson(n, x) is the corresponding cumulation distribution. &lt;br /&gt;
&lt;br /&gt;
In Erlang-C function, there are three key elements: &lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; 1. Probability of delay, P_c.&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; 2. # of servers, n.&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; 3. Offered Traffic Load, x. &lt;br /&gt;
&lt;br /&gt;
Given any two, the last parameter can be calculated or approximated. For example, in the code pieces below:&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; 1. Function &lt;i&gt;&lt;b&gt;ErlcFractionDelay(Nsrv, traffErlang)&lt;/b&gt;&lt;/i&gt; calculates Probability of delay given number of servers and offered traffic Load in Erlangs units. &lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; 2. Function &lt;i&gt;&lt;b&gt;ErlcNsrv(traffErlang, Erlc_max)&lt;/b&gt;&lt;/i&gt; estimates the minimum number servers to reach below the max probability of delay given by Erlc_max for a given offered traffic load of traffErlang.&lt;br /&gt;
&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; 3. Function &lt;i&gt;&lt;b&gt;ErlcTrafFromFractionDelay(Nsrv, fractionDelay)&lt;/b&gt;&lt;/i&gt; Compute the max traffic load that Nserv servers can handle with queuing probability less or equal to fractionDelay. &lt;br /&gt;
&lt;br /&gt;
In addition to the three key numbers in Erlang-C model, there are some other paremeters of particular interests to Call Center management. For example, the probability an arriving call will be put in queue for no more than certain waiting time given the average handling time. Function &lt;b&gt;&lt;i&gt;ErlcGoS(Nsrv, traffErlang, AHT, AWT)&lt;/i&gt;&lt;/b&gt; will calcualte this quantity.&lt;br /&gt;
&lt;br /&gt;
The results of this set of functions has been calibrated against the data from [5], specifically Table 1. of [5] is reproduced exactly. The DATA STEP at the end of the program below will reproduce the results, and here is the result my own run.&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiphwrhcYLRNzcqX1EAAuZuX4TTf65CTXIIts0oCeJ-uOblzM8FNQd1kOX6lzcY9KN9rHTKQYJUvfCnOKeKt5QRHxCtUWVX07fOFCUiCZ1yB8UlIcokHbAEpRxkKcyU7eY-aVfGZg/s1600/TABLE1_Replicate.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;380&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiphwrhcYLRNzcqX1EAAuZuX4TTf65CTXIIts0oCeJ-uOblzM8FNQd1kOX6lzcY9KN9rHTKQYJUvfCnOKeKt5QRHxCtUWVX07fOFCUiCZ1yB8UlIcokHbAEpRxkKcyU7eY-aVfGZg/s640/TABLE1_Replicate.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
For a complete list of quantities that Erlang-C model will work with and compute for, the webpage of Abstract Micro Systems&amp;nbsp;@ &lt;a href=&quot;http://abstractmicro.com/erlang/helppages/mod-c.htm&quot;&gt;http://abstractmicro.com/erlang/helppages/mod-c.htm&lt;/a&gt; provides a very good introduction.&lt;br /&gt;
&lt;br /&gt;
&lt;b&gt;&lt;u&gt;Reference:&lt;/u&gt;&lt;/b&gt;&lt;br /&gt;
[1]. Kleinrock, Leonard (January 1975). &lt;i&gt;Queueing Systems: Volume I – Theory&lt;/i&gt;. New York: Wiley Interscience.&amp;nbsp;ISBN 978-0471491101. &lt;br /&gt;
&lt;br /&gt;
[2]. Kleinrock, Leonard (April 1976). &lt;i&gt;Queueing Systems: Volume II – Computer Applications&lt;/i&gt;. New York: Wiley Interscience.&amp;nbsp;ISBN 978-0471491118. &lt;br /&gt;
&lt;br /&gt;
[3]. &lt;a href=&quot;http://www.listserv.uga.edu/cgi-bin/wa?A2=ind0210C&amp;amp;L=sas-l&amp;amp;P=R10763&quot;&gt;http://www.listserv.uga.edu/cgi-bin/wa?A2=ind0210C&amp;amp;L=sas-l&amp;amp;P=R10763&lt;/a&gt;&lt;br /&gt;
&lt;br /&gt;
[4]. &lt;a href=&quot;http://www.listserv.uga.edu/cgi-bin/wa?A2=ind1206C&amp;amp;L=sas-l&amp;amp;P=R45472&quot;&gt;http://www.listserv.uga.edu/cgi-bin/wa?A2=ind1206C&amp;amp;L=sas-l&amp;amp;P=R45472&lt;/a&gt;&lt;br /&gt;
&lt;br /&gt;
[5]. Tibor Mišuth, Erik Chromý, Ivan Baroňák. &lt;i&gt;Method for Fast Estimation of Contact Centre Parameters Using Erlang-C Model&lt;/i&gt;, in Proceedings of 2010 Third International Conference on Communication Theory, Reliability, and Quality of Service, DOI 10.1109/CTRQ.2010.38&lt;br /&gt;
&amp;nbsp; &lt;br /&gt;
All functions below have relatively detailed comments. The functions are provided as is without any implied and explicated warranties, and are&amp;nbsp;for research purpose only. &lt;br /&gt;
&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border-bottom: #999999 1px dashed; border-left: #999999 1px dashed; border-right: #999999 1px dashed; border-top: #999999 1px dashed; color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; width: 100%;&quot;&gt;&lt;code&gt;

proc fcmp outlib=sasuser.funcs.Erlc;   

     function ErlcFractionDelay(Nsrv, traffErlang); 
  /*
        Entry        : ErlcFractionDelay
     Purpose      : Compute the Probability of Delay when an arriving call is not
                    answered immediately and put in queue
     Usage        : P=ErlcFractionDelay(Nsrv, traffErlang)
     Inputs       : Nsrv=Number of Server (CSR)
                    traffErlang=Traffic in Erlang
     Handle Error : Return -1 if traffic in Erlang is larger than the number of 
                           servers;
                    Return -2 if either traffic in Erlang or Number of servers is 
                           negative;
                    Return -3 if number of servers is not integer;                          
  */
         if traffErlang&amp;gt;Nsrv then do;
            *put &quot;Erlang amount should be smaller than Number of Servers&quot;;
   Ec=-1; return(Ec);
         end; 
   else if (traffErlang&amp;lt;0) or (Nsrv&amp;lt;0) then do;
            *put &quot;Both Erlang amount and Number of Servers should be larger than 0&quot;;
   Ec=-2; return(Ec);
   end;
   else if abs(int(Nsrv)-Nsrv)&amp;gt;1E-8 then do;
            *put &quot;Number of Servers should be integer&quot;;
   Ec=-3; return(Ec);
   end;  
         else do; 
            _p= PDF(&#39;POISSON&#39;, Nsrv, traffErlang);
         denom = _p
                  + (1-traffErlang/Nsrv)*CDF(&#39;POISSON&#39;, Nsrv-1, traffErlang);
            Ec = _p / denom;
            return (Ec);
   end;
     endsub;

  function ErlcGoS(Nsrv, traffErlang, AHT, AWT);
  /*
        Entry        : ErlcGoS
     Purpose      : Compute the Probability an arriving call will be put in
                    queue and wait for no more than AWT when the average
                    handling time is AHT. This term is also called Grade of 
                    Service (GoS). Note that the probability that an
                    arriving call will NOT be put in queue is:
                      P=1-ErlcFractionDelay();
     Usage        : GoS=ErlcGoS(Nsrv, traffErlang, AHT, AWT)
     Inputs       : Nsrv=Number of Server (CSR)
                    traffErlang=Traffic in Erlang
                    AHT=Average Handling Time
                    AWT=Average Waiting Time
     Handle Error : Return -1 if traffic in Erlang is larger than the number of 
                           servers;
                    Return -2 if either traffic in Erlang or Number of servers is 
                           negative;
                    Return -3 if number of servers is not integer;     
                       Return -4 if either of two time parameters is negative; 
  */
         if traffErlang&amp;gt;Nsrv then do;
            *put &quot;Erlang amount should be smaller than Number of Servers&quot;;
      GoS=-1; return(GoS);
         end; 
   else if (traffErlang&amp;lt;0) or (Nsrv&amp;lt;0) then do;
            *put &quot;Both Erlang amount and Number of Servers should be larger than 0&quot;;
   GoS=-2; return(GoS);
   end;
   else if  abs(int(Nsrv)-Nsrv)&amp;gt;1E-8 then do;
            *put &quot;Number of Servers should be integer&quot;;
   GoS=-3; return(GoS);
   end;   
         else  if AHT&amp;lt;0 or AWT&amp;lt;0 then do;
            put &quot;Both Average Holding Time and Average Waiting Time should be &amp;gt;0&quot;; 
   GoS=-4; return(GoS);
   end;
   else do;
      GoS = 1-ErlcFractionDelay(Nsrv, traffErlang)*exp(-1*(Nsrv-traffErlang)*AWT/AHT);
      return(GoS);
    end;
  endsub;

  function ErlcNsrv(traffErlang, Erlc_max);
  /*
        Entry        : ErlcNsrv
     Purpose      : Compute the required number of servers (CSRs) to handle
                    given traffic load with queuing probability no more than
                    Erlc_max
     Usage        : N=ErlcNsrv(traffErlang, Erlc_max)
     Inputs       : traffErlang=Traffic load in Erlang
                    Erlc_max=max Queuing probability
     Handle Error : Return -1 if max queuing probability is not within (0, 1) 
                           range so to violate probability definition;
                    Return -2 if traffic in Erlang is negative;
  */

  /*REF:
       Algorithm is based on that from paper &quot;Method for Fast 
             Estimation of Contact Centre Parameters Using 
             Erlang C Model&quot; by Tibor Mišuth, Erik Chromý 
             and Ivan Baronák from Slovak University of 
             Technology. This paper appeared at &quot;The Third 
             International Conference on Communication 
             Theory, Reliability, and Quality of Service&quot;, 
             2010
  */
        if Erlc_max&amp;gt;=1 or Erlc_max&amp;lt;=0 then do;
        *put &quot;Max Erlang-C value should be between 0 and 1&quot;;
     N=-1; return(N);
     end;
     else if traffErlang&amp;lt;0 then do;
              *put &quot;Traffic in Erlang should be &amp;gt;0&quot;;
     N=-2; return(N);
     end;
     else do;
           _k=floor(traffErlang)+1; s=0;
           do N=1 to _k; 
        s=(1+s)*N/traffErlang;
              end;
              _threshold=(1-Erlc_max)/((1-traffErlang/N)*Erlc_max);
              do while (s&amp;lt;=_threshold);
                 N=N+1;
        s=(1+s)*N/traffErlang;
        _threshold=(1-Erlc_max)/((1-traffErlang/N)*Erlc_max);
     end;
              return(N);
     end;
  endsub;

  function ErlcNsrvFromGoS( traffErlang, AHT, AWT, GoS);
  /*
        Entry        : ErlcNsrvFromGoS
     Purpose      : Compute the required number of servers (CSRs) to handle
                    given traffic load with given service level (GoS):
                           Average Handling Time /
                           the Probability an arriving call is put in queue
                           and wait no more than Average Waiting Time
     Usage        : N=ErlcNsrvFromGoS( traffErlang, AHT, AWT, GoS)
     Inputs       : traffErlang=Traffic load in Erlang
                    AHT=Average Handling Time
                    AWT=Average Waiting Time
                    GoS=probability an arriving Call in put in queue
                        but wait no more than AWT
     Handle Error : Return -1 if either time parameter is negative;
                    Return -2 if queuing probability is not in (0, 1) range;
                    Return -3 if traffic in Erlang is negative
  */
         if AHT&amp;lt;0 or AWT&amp;lt;0 then do;
            *put &quot;Both Average Holding Time and Average Waiting Time should be &amp;gt;0&quot;; 
      N0=-1; return(N0);
   end; 
   else if GoS&amp;lt;0 or GoS&amp;gt;1 then do;
      *put &quot;GoS should be between 0 and 1&quot;;
   N0=-2; return(N0);
   end;
   else if traffErlang&amp;lt;0 then do;
            *put &quot;Traffic in Erlang must be positive real number&quot;;
   N0=-3; return(N0);
   end;
         else do; 
            N0=floor(traffErlang)+1;
         _GoS=ErlcGoS(N0, traffErlang, AHT, AWT);
         do while (_GoS&amp;lt;=GoS);
            N0=N0+1;
            _GoS=ErlcGoS(N0, traffErlang, AHT, AWT);
         end;
         return(N0);
   end;
  endsub;

  function ErlcNsrvFromWait( traffErlang, AHT, AWT);
  /*
        Entry        : ErlcNsrvFromWait
     Purpose      : Compute the required number of servers (CSRs) to handle
                    given traffic load with given Average Handling Time and
                    no more than given Average Waiting Time
     Usage        : N=ErlcNsrvFromWait( traffErlang, AHT, AWT)
     Inputs       : traffErlang=Traffic load in Erlang
                    AHT=Average Handling Time
                    AWT=Average Waiting Time                    
     Handle Error : Return -1 if either time parameter is negative;                   
                    Return -2 if traffic in Erlang is negative
  */
         if AHT&amp;lt;0 or AWT&amp;lt;0 then do;
            *put &quot;Both Average Holding Time and Average Waiting Time should be &amp;gt;0&quot;; 
   Nsrv=-1; return(Nsrv);
   end;
   else if traffErlang&amp;lt;0 then do;
            *put &quot;Traffic in Erlang must be positive real number&quot;;
   N0=-2; return(N0);
   end;
   else do;
           Nsrv=floor(traffErlang)+1;
           c=ErlcWait (Nsrv, traffErlang, AHT);
           do while(c&amp;gt;AWT);
                 Nsrv=Nsrv+1;
           c=ErlcWait (Nsrv, traffErlang, AHT);;
            end;
            return (Nsrv);
   end;
  endsub;

  function ErlcNwaiting(Nsrv, traffErlang);
  /*
        Entry        : ErlcNwaiting
     Purpose      : Compute the average number of calls waiting 
                    in queue, given number of servers and traffic
                    in Erlang
     Usage        : N=ErlcNwaiting(Nsrv, traffErlang)
     Inputs       : Nsrv=Number of Server
                    traffErlang=Traffic load in Erlang       
     Handle Error : Return -1 if traffic in Erlang is larger than number
                              of servers;                   
                    Return -2 if number of servers is negative
                    Return -3 if traffic in Erlang is negative
                    Return -4 if number of servers is not integer
  */
         if traffErlang&amp;gt;Nsrv then do;
            *put &quot;Erlang amount should be smaller than Number of Servers&quot;;
   Q=-1; return(Q);
         end; 
   else if (Nsrv&amp;lt;0) then do;
            *put &quot;Both Erlang amount and Number of Servers should be larger than 0&quot;;
   Q=-2; return(Q);
   end;
   else if traffErlang&amp;lt;0 then do;
            *put &quot;Traffic in Erlang must be positive real number&quot;;
   Q=-3; return(Q);
   end;
   else if  abs(int(Nsrv)-Nsrv)&amp;gt;1E-8 then do;
            *put &quot;Number of Servers should be integer&quot;;
   Q=-4; return(Q);
   end;  
         else do; 
            Q=traffErlang/(Nsrv-traffErlang)*ErlcFractionDelay(Nsrv, traffErlang);
      return(Q);
   end;
  endsub;

  function ErlcTrafFromFractionDelay(Nsrv, fractionDelay);
  /*
        Entry        : ErlcTrafFromFractionDelay
     Purpose      : Compute the max traffic load that N servers
                    can handle with queuing probability less or equal
                    to fractionDelay. 
     Usage        : A=ErlcTrafFromFractionDelay(Nsrv, fractionDelay)
     Inputs       : Nsrv=Number of Server
                    fractionDelay=Probability of waiting in queue
                                  when a call arrives                    
     Handle Error : Return -1 if probability of waiting in Queue is not
                              in range of (0, 1) 
                    Return -2 if number of servers is negative                   
                    Return -3 if number of servers is not integer
  */
  /*  NOTE:
         Using Bisection Method to solve for Erlang 
      given N server and Erlang C number. Bisection 
      is simple and robust. Speed is not of primiary
      concern here.
  */
            if fractionDelay&amp;gt;1 or fractionDelay&amp;lt;0  then do;
                *put &quot;Number of servers must be positive integers&quot;;
    c=-1; return(c);
    end;
    else if Nsrv&amp;lt;=0 then do;  
                *put &quot;Fraction of Delay must be real number between 0 and 1&quot;;
    c=-2; return(c);
    end;
    else if abs(int(Nsrv)-Nsrv)&amp;gt;1E-8 then do;;
       c=-3; return(c);
    end;
    else do;
                min=1E-8; max=Nsrv-1E-8;
             itermax=500; iter=1;
             do while (iter&amp;lt;=itermax);
                   c=(min+max)/2;
             _x=ErlcFractionDelay(Nsrv, c)-fractionDelay;
             if abs(_x)&amp;lt;1E-8 or abs(max-min)/2&amp;lt;1E-8 then do;                    
                iter=itermax+1;
             end;
             else do;
                      iter=iter+1;
       _tmp=ErlcFractionDelay(Nsrv, max)-fractionDelay;
                if sign(_x)=sign(_tmp) then max=c;
                      else min=c;
             end;
            end;      
      return (c);
   end;
  endsub;

  function ErlcWait (Nsrv, traffErlang, AHT);
  /*
        Entry        : ErlcWait
     Purpose      : Compute the Average Speed of Answer (ASA, also called
                    Average Waiting Time, AWT), given the number of servers, 
                    traffic in Erlang and average handling time. 
     Usage        : N=ErlcWait(Nsrv, traffErlang, AHT)
     Inputs       : Nsrv=Number of Server
                    traffErlang=Traffic load in Erlang       
                    AHT=Average Handling Time
     Handle Error : Return -1 if Average Handling Time is negative           
                    Return -2 if number of servers is negative
                    Return -3 if traffic in Erlang is negative
                    Return -4 if number of servers is not integer
                    Return -5 if traffic in Erlang is larger than the
                           number of servers
  */
      if AHT&amp;lt;0 then do;
            *put &quot;Average Holding Time should be &amp;gt;0&quot;; 
      ASA=-1; return(ASA);
   end;
   else if (Nsrv&amp;lt;0) then do;
            *put &quot;Both Erlang amount and Number of Servers should be larger than 0&quot;;
   ASA=-2; return(ASA);
   end;
   else if traffErlang&amp;lt;0 then do;
            *put &quot;Traffic in Erlang must be positive real number&quot;;
   ASA=-3; return(ASA);
   end;
   else if  abs(int(Nsrv)-Nsrv)&amp;gt;1E-8 then do;
            *put &quot;Number of Servers should be integer&quot;;
   ASA=-4; return(ASA);
   end; 
   else if traffErlang&amp;gt;Nsrv then do;
      ASA=-5; return(ASA);
   end;  
   else do;
            ASA=ErlcFractionDelay(Nsrv, traffErlang)*AHT/(Nsrv-traffErlang);
      return(ASA);
   end;
  endsub;

  function ErlcK (Nsrv, traffErlang);
  /*
        Entry        : ErlcK
     Purpose      : Compute the average number of calls in queue, given 
                    number of servers and traffic in Erlang. It is the 
                    sum of average number of calls being served (traffErlang) 
                    and average number of calls waiting in queue.
     Usage        : N=ErlcK(Nsrv, traffErlang)
     Inputs       : Nsrv=Number of Server
                    traffErlang=Traffic load in Erlang       
     Handle Error : Return -1 if traffic in Erlang is larger than number
                              of servers;                   
                    Return -2 if number of servers is negative
                    Return -3 if traffic in Erlang is negative
                    Return -4 if number of servers is not integer
  */
        if traffErlang&amp;gt;Nsrv then do;
            *put &quot;Erlang amount should be smaller than Number of Servers&quot;;
   K=-1; return(K);
         end; 
      else if (Nsrv&amp;lt;0) then do;
            *put &quot;Both Erlang amount and Number of Servers should be larger than 0&quot;;
   K=-2; return(K);
   end;
   else if traffErlang&amp;lt;0 then do;
            *put &quot;Traffic in Erlang must be positive real number&quot;;
   K=-3; return(K);
   end;
   else if  abs(int(Nsrv)-Nsrv)&amp;gt;1E-8 then do;
            *put &quot;Number of Servers should be integer&quot;;
   K=-4; return(K);
   end;  
   else do;
            K = traffErlang*(1+ ErlcFractionDelay(Nsrv, traffErlang)/(Nsrv-traffErlang));
      return (K);
   end;
  endsub;

  function ErlcT( Nsrv, traffErlang, AHT);
  /*
        Entry        : ErlcT
     Purpose      : Compute the average time a call will spend in the system,
                    which is the sum of average handling time and average 
                       waiting time (ASA). Three relavent parameters are
                    number of servers, traffic in Erlang and average handling
                    time. 
     Usage        : N=ErlcT(Nsrv, traffErlang, AHT)
     Inputs       : Nsrv=Number of Server
                    traffErlang=Traffic load in Erlang   
                       AHT=Average Handling Time 
     Handle Error : Return -1 if average handling time is negative;                   
                    Return -2 if number of servers is negative
                    Return -3 if traffic in Erlang is negative
                    Return -4 if number of servers is not integer
                    Return -5 if traffic in Erlang is larger than number 
                           of servers
  */
      if AHT&amp;lt;0 then do;
            *put &quot;Average Holding Time should be &amp;gt;0&quot;; 
      T=-1; return(T);
   end;
   else if (Nsrv&amp;lt;0) then do;
            *put &quot;Both Erlang amount and Number of Servers should be larger than 0&quot;;
   T=-2; return(T);
   end;
   else if traffErlang&amp;lt;0 then do;
            *put &quot;Traffic in Erlang must be positive real number&quot;;
   T=-3; return(T);
   end;
   else if  abs(int(Nsrv)-Nsrv)&amp;gt;1E-8 then do;
            *put &quot;Number of Servers should be integer&quot;;
   T=-4; return(T);
   end; 
   else if traffErlang&amp;gt;Nsrv then do;
      T=-5; return(T);
   end;
         else do; 
            T=AHT + AHT*ErlcFractionDelay(Nsrv, traffErlang)/((Nsrv-traffErlang));
      return(T);
   end;
  endsub;

run;
options cmplib=sasuser.funcs;


data _null_;
     lambda=667; AHT=150; AWT=20;
  A=lambda*150/3600;
  
  do j=1 to 10;
     N=floor(A)+j;
     Pc=ErlcFractionDelay(N, A);
  GoS=ErlcGoS(N, A, AHT, AWT); 
        Q=ErlcNwaiting(N, A);
        T=ErlcT(N, A, AHT); 
  K=ErlcK(N, A);
  rho=A/N;  
  put N=  @8  Pc= percent.4  @16 K= bestd8.1    @26  T= bestd8.1 
                @38 Q= bestd8.1    @50 Gos= bestd8.1  @62  rho= percent8.4;
  end;
run;
&lt;/code&gt;&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/1806703496617507180/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/1806703496617507180' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/1806703496617507180'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/1806703496617507180'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2012/07/sas-functions-for-computing-parameters.html' title='SAS functions for computing parameters in Erlang-C model'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiphwrhcYLRNzcqX1EAAuZuX4TTf65CTXIIts0oCeJ-uOblzM8FNQd1kOX6lzcY9KN9rHTKQYJUvfCnOKeKt5QRHxCtUWVX07fOFCUiCZ1yB8UlIcokHbAEpRxkKcyU7eY-aVfGZg/s72-c/TABLE1_Replicate.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-29815492.post-2505374346425236390</id><published>2012-05-24T05:19:00.001-07:00</published><updated>2012-05-25T10:23:16.314-07:00</updated><title type='text'>Stochastic Gradient Decending Logistic Regression in SAS</title><content type='html'>Test the Stochastic Gradient Decending Logistic Regression in SAS. The logic and code follows the code piece of Ravi Varadhan, Ph.D from this &lt;a href=&quot;http://r.789695.n4.nabble.com/Stochastic-Gradient-Ascent-for-logistic-regression-td884272.html&quot; target=&quot;_blank&quot;&gt;&lt;strong&gt;&lt;em&gt;discussion&lt;/em&gt;&lt;/strong&gt; &lt;/a&gt;of R Help. The blog &lt;a href=&quot;http://sasdiehard.blogspot.com/&quot; target=&quot;_blank&quot;&gt;&lt;strong&gt;&lt;em&gt;SAS Die Hard&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt; also has a post about SGD Logistic Regression in SAS.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border-bottom: #999999 1px dashed; border-left: #999999 1px dashed; border-right: #999999 1px dashed; border-top: #999999 1px dashed; color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; width: 100%;&quot;&gt;&lt;code&gt;

filename foo url &quot;http://www.biostat.jhsph.edu/~ririzarr/Teaching/754/lbw.dat&quot; ;

data temp;
   infile foo length=len;
   input low age lwt race smoke ptl ht ui ftv bwt;
   put low age lwt race smoke ptl ht ui ftv bwt;
   if _n_&amp;gt;1;
run;
proc standard data=temp out=temp mean=0  std=1;
      var  age lwt smoke ht ui;
run;

proc contents data=temp  out=vars(keep=varnum  name type) noprint; run;

proc sql noprint;
      select name into :covars separated by &quot; &quot;
   from   vars
   where  lowcase(name) in (&quot;age&quot;, &quot;lwt&quot;, &quot;smoke&quot;, &quot;ht&quot;, &quot;ui&quot;)
   ;
   select cats(&quot;b_&quot;, name) into :covars2 separated by &quot; &quot;
   from   vars
   where  lowcase(name) in (&quot;age&quot;, &quot;lwt&quot;, &quot;smoke&quot;, &quot;ht&quot;, &quot;ui&quot;)
   ; 
     select count(*)+1 into :nparms
     from   vars
  where  lowcase(name) in (&quot;age&quot;, &quot;lwt&quot;, &quot;smoke&quot;, &quot;ht&quot;, &quot;ui&quot;)
  ;
quit;
%put &amp;amp;covars2;

%lr_sgd(temp, beta, low, &amp;amp;covars, 
        alpha=0.01, decay=0.98, 
        criterion=0.00001, maxiter=1000);


options fullstimer;
proc logistic data=temp  outest=_beta  desc noprint;
      model low = age lwt smoke ht ui;
run;

&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
Execution log shows:&lt;br /&gt;
******************************************************************************************&lt;br /&gt;
606&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
607 %lr_sgd(temp, beta, low, &amp;amp;covars, alpha=0.01, decay=0.98, criterion=0.00001, maxiter=1000);&lt;br /&gt;
&lt;br /&gt;
MLOGIC(LR_SGD): Beginning execution.&lt;br /&gt;
&lt;br /&gt;
MLOGIC(LR_SGD): Parameter DSN has value temp&lt;br /&gt;
&lt;br /&gt;
MLOGIC(LR_SGD): Parameter OUTEST has value beta&lt;br /&gt;
&lt;br /&gt;
MLOGIC(LR_SGD): Parameter RESPONSE has value low&lt;br /&gt;
&lt;br /&gt;
MLOGIC(LR_SGD): Parameter COVARS has value age ht lwt smoke ui&lt;br /&gt;
&lt;br /&gt;
MLOGIC(LR_SGD): Parameter ALPHA has value 0.01&lt;br /&gt;
&lt;br /&gt;
MLOGIC(LR_SGD): Parameter DECAY has value 0.98&lt;br /&gt;
&lt;br /&gt;
MLOGIC(LR_SGD): Parameter CRITERION has value 0.00001&lt;br /&gt;
&lt;br /&gt;
MLOGIC(LR_SGD): Parameter MAXITER has value 1000&lt;br /&gt;
&lt;br /&gt;
MPRINT(LR_SGD): options nosource nonotes;&lt;br /&gt;
&lt;br /&gt;
MPRINT(LR_SGD): options nomlogic nomprint;&lt;br /&gt;
&lt;br /&gt;
Iteration 1, time used 0.046, converge criteria is 0.355&lt;br /&gt;
&lt;br /&gt;
Iteration 2, time used 0.032, converge criteria is 0.1886732527&lt;br /&gt;
&lt;br /&gt;
Iteration 3, time used 0.14, converge criteria is 0.110486903&lt;br /&gt;
&lt;br /&gt;
Iteration 4, time used 0.031, converge criteria is 0.0694367942&lt;br /&gt;
&lt;br /&gt;
Iteration 5, time used 0.031, converge criteria is 0.0458381768&lt;br /&gt;
&lt;br /&gt;
Iteration 6, time used 0.032, converge criteria is 0.0313858459&lt;br /&gt;
&lt;br /&gt;
Iteration 7, time used 0.031, converge criteria is 0.0221141453&lt;br /&gt;
&lt;br /&gt;
Iteration 8, time used 0.047, converge criteria is 0.0159488055&lt;br /&gt;
&lt;br /&gt;
Iteration 9, time used 0.031, converge criteria is 0.0125402184&lt;br /&gt;
&lt;br /&gt;
Iteration 10, time used 0.031, converge criteria is 0.0102548103&lt;br /&gt;
&lt;br /&gt;
Iteration 11, time used 0.031, converge criteria is 0.008416673&lt;br /&gt;
&lt;br /&gt;
Iteration 12, time used 0.047, converge criteria is 0.0069325467&lt;br /&gt;
&lt;br /&gt;
Iteration 13, time used 0.032, converge criteria is 0.0057299824&lt;br /&gt;
&lt;br /&gt;
Iteration 14, time used 0.031, converge criteria is 0.0047522323&lt;br /&gt;
&lt;br /&gt;
Iteration 15, time used 0.031, converge criteria is 0.0039546116&lt;br /&gt;
&lt;br /&gt;
Iteration 16, time used 0.031, converge criteria is 0.0033017935&lt;br /&gt;
&lt;br /&gt;
Iteration 17, time used 0.047, converge criteria is 0.0027657536&lt;br /&gt;
&lt;br /&gt;
Iteration 18, time used 0.031, converge criteria is 0.0023241894&lt;br /&gt;
&lt;br /&gt;
Iteration 19, time used 0.032, converge criteria is 0.0019592976&lt;br /&gt;
&lt;br /&gt;
Iteration 20, time used 0.046, converge criteria is 0.0016568231&lt;br /&gt;
&lt;br /&gt;
Iteration 21, time used 0.032, converge criteria is 0.0014053171&lt;br /&gt;
&lt;br /&gt;
Iteration 22, time used 0.031, converge criteria is 0.0011955575&lt;br /&gt;
&lt;br /&gt;
Iteration 23, time used 0.031, converge criteria is 0.0010200936&lt;br /&gt;
&lt;br /&gt;
Iteration 24, time used 0.047, converge criteria is 0.0008728877&lt;br /&gt;
&lt;br /&gt;
Iteration 25, time used 0.031, converge criteria is 0.0007490333&lt;br /&gt;
&lt;br /&gt;
Iteration 26, time used 0.032, converge criteria is 0.0006445313&lt;br /&gt;
&lt;br /&gt;
Iteration 27, time used 0.031, converge criteria is 0.0005561129&lt;br /&gt;
&lt;br /&gt;
Iteration 28, time used 0.047, converge criteria is 0.0004810987&lt;br /&gt;
&lt;br /&gt;
Iteration 29, time used 0.031, converge criteria is 0.0004172864&lt;br /&gt;
&lt;br /&gt;
Iteration 30, time used 0.031, converge criteria is 0.0003628606&lt;br /&gt;
&lt;br /&gt;
Iteration 31, time used 0.031, converge criteria is 0.0003163211&lt;br /&gt;
&lt;br /&gt;
Iteration 32, time used 0.032, converge criteria is 0.0002764247&lt;br /&gt;
&lt;br /&gt;
Iteration 33, time used 0.047, converge criteria is 0.0002421384&lt;br /&gt;
&lt;br /&gt;
Iteration 34, time used 0.031, converge criteria is 0.0002126018&lt;br /&gt;
&lt;br /&gt;
Iteration 35, time used 0.047, converge criteria is 0.0001870963&lt;br /&gt;
&lt;br /&gt;
Iteration 36, time used 0.046, converge criteria is 0.0001650204&lt;br /&gt;
&lt;br /&gt;
Iteration 37, time used 0.032, converge criteria is 0.0001458692&lt;br /&gt;
&lt;br /&gt;
Iteration 38, time used 0.031, converge criteria is 0.000129218&lt;br /&gt;
&lt;br /&gt;
Iteration 39, time used 0.031, converge criteria is 0.0001147086&lt;br /&gt;
&lt;br /&gt;
Iteration 40, time used 0.047, converge criteria is 0.0001020383&lt;br /&gt;
&lt;br /&gt;
Iteration 41, time used 0.031, converge criteria is 0.0000909506&lt;br /&gt;
&lt;br /&gt;
Iteration 42, time used 0.032, converge criteria is 0.0000812278&lt;br /&gt;
&lt;br /&gt;
Iteration 43, time used 0.031, converge criteria is 0.0000726846&lt;br /&gt;
&lt;br /&gt;
Iteration 44, time used 0.047, converge criteria is 0.0000651629&lt;br /&gt;
&lt;br /&gt;
Iteration 45, time used 0.031, converge criteria is 0.0000585277&lt;br /&gt;
&lt;br /&gt;
Iteration 46, time used 0.031, converge criteria is 0.0000526634&lt;br /&gt;
&lt;br /&gt;
Iteration 47, time used 0.031, converge criteria is 0.0000474707&lt;br /&gt;
&lt;br /&gt;
Iteration 48, time used 0.047, converge criteria is 0.0000428642&lt;br /&gt;
&lt;br /&gt;
Iteration 49, time used 0.063, converge criteria is 0.0000387705&lt;br /&gt;
&lt;br /&gt;
Iteration 50, time used 0.031, converge criteria is 0.0000351261&lt;br /&gt;
&lt;br /&gt;
Iteration 51, time used 0.031, converge criteria is 0.000031876&lt;br /&gt;
&lt;br /&gt;
Iteration 52, time used 0.032, converge criteria is 0.0000289726&lt;br /&gt;
&lt;br /&gt;
Iteration 53, time used 0.031, converge criteria is 0.0000263748&lt;br /&gt;
&lt;br /&gt;
Iteration 54, time used 0.031, converge criteria is 0.0000240465&lt;br /&gt;
&lt;br /&gt;
Iteration 55, time used 0.031, converge criteria is 0.0000219565&lt;br /&gt;
&lt;br /&gt;
Iteration 56, time used 0.031, converge criteria is 0.0000200774&lt;br /&gt;
&lt;br /&gt;
Iteration 57, time used 0.047, converge criteria is 0.0000183854&lt;br /&gt;
&lt;br /&gt;
Iteration 58, time used 0.172, converge criteria is 0.0000168595&lt;br /&gt;
&lt;br /&gt;
Iteration 59, time used 0.031, converge criteria is 0.0000154815&lt;br /&gt;
&lt;br /&gt;
Iteration 60, time used 0.032, converge criteria is 0.0000142352&lt;br /&gt;
&lt;br /&gt;
Iteration 61, time used 0.031, converge criteria is 0.0000131064&lt;br /&gt;
&lt;br /&gt;
Iteration 62, time used 0.031, converge criteria is 0.0000120826&lt;br /&gt;
&lt;br /&gt;
Iteration 63, time used 0.031, converge criteria is 0.0000111528&lt;br /&gt;
&lt;br /&gt;
Iteration 64, time used 0.032, converge criteria is 0.0000103072&lt;br /&gt;
&lt;br /&gt;
Iteration 65, time used 0.031, converge criteria is 9.5372542E-6&lt;br /&gt;
&lt;br /&gt;
Total Time is 2.56 sec.&lt;br /&gt;
&lt;br /&gt;
Total Iteration is 65, convergence status is Converged.&lt;br /&gt;
&lt;br /&gt;
At Final Iteration, max difference is 9.5372542E-6&lt;br /&gt;
&lt;br /&gt;
MPRINT(LR_SGD): source;&lt;br /&gt;
&lt;br /&gt;
MLOGIC(LR_SGD): Ending execution.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
******************************************************************************************&lt;br /&gt;
&lt;br /&gt;
The macro %LR_SGD. &lt;br /&gt;
&lt;pre style=&quot;background-color: #ebebeb; border-bottom: #999999 1px dashed; border-left: #999999 1px dashed; border-right: #999999 1px dashed; border-top: #999999 1px dashed; color: #000001; font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; font-size: 12px; line-height: 14px; overflow: auto; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; width: 100%;&quot;&gt;&lt;code&gt;
/*
  SAS macro:
     Logistic Regression using Stochastic Gradient Descent.    
  Name: 
     %ls_sgd();
  Copyright (c) 2009, Liang Xie (Contact me @ xie1978 at gmail dot com)
  
  
  The SAS macro is a demonstration of an implementation of logistic 
  regression modelstrained by Stochastic Gradient Decent (SGD).This 
  program reads a training set specified as &amp;amp;dsn_in, trains a logistic 
  regression model, and outputs the estimated coefficients to &amp;amp;outest. 
  Example usage:

  %let inputdata=train_data;
  %let beta=coefficient;
  %let response=Event;
  %lr_sgd(&amp;amp;inputdata, &amp;amp;beta, &amp;amp;response, &amp;amp;covars, 
          alpha=0.008, decay=0.8, 
          criterion=0.00001, maxiter=1000);


  The following topics are not covered for simplicity:    
      - bias term    
      - regularization    
      - multiclass logistic regression (maximum entropy model)         
      - calibration of learning rate

  &lt;i&gt;Distributed under GNU Affero General Public License version 3. This 
  program is free software: you can redistribute it and/or modify
  it under the terms of the GNU Affero General Public License as
  published by the Free Software Foundation, only version 3 of the
  License.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  GNU Affero General Public License for more details. 

&lt;/i&gt;*/

%macro logistic(dsn_in, outest, response, alpha=0.0005);
proc score data=&amp;amp;dsn_in  score=&amp;amp;outest  type=parms  out=score(keep=score);
     var intercept &amp;amp;covars;
run;

data _xtemp/view=_xtemp;
     merge &amp;amp;dsn score ;
     _w=&amp;amp;response - 1/(1+exp(-score));
  /*
  array x{*} intercept &amp;amp;covars;
  _w=&amp;amp;response - 1/(1+exp(-score));
  do i=1 to dim(x); x[i]=x[i]*_w; end;    
  */
run;

data _x&amp;amp;outest;
  array x{*} intercept &amp;amp;covars;
  array _x{*} b_intercept &amp;amp;covars2;
  retain b_intercept &amp;amp;covars2;
  retain logneg logpos 0;
  modify _x&amp;amp;outest;
  do i=1 to dim(x); x[i]=_x[i]; end;
  
  do until (eof);
        set _xtemp  end=eof;
     do  i=1 to dim(x);
         _x[i]=_x[i]+&amp;amp;alpha*x[i]*_w;
     end;
  end;
     replace;
run;

%mend;

%macro compare(dsn1, dsn2);
data _null_;
     merge &amp;amp;dsn1  &amp;amp;dsn2;
  array _x1{*} intercept &amp;amp;covars;
  array _x2{*} b_intercept &amp;amp;covars2;
  retain maxdiff 0;
  do i=1 to dim(_x1);      
     maxdiff=max(maxdiff, abs(_x1[i]-_x2[i]));  
  *put _x1[*]=;
  *put _x2[*]=;
  end;
  call symput(&#39;maxdiff&#39;, maxdiff);
run;
%mend;




%macro lr_sgd(dsn, outest, response, covars, 
              alpha=0.0005, decay=0.9, 
              criterion=0.00001, maxiter=1000);
options nosource nonotes;
options nomlogic nomprint;
%local i t0 t1 dt maxdiff status  stopiter;

%let t00=%sysfunc(datetime());

data &amp;amp;dsn;
     set &amp;amp;dsn;
  intercept=1; _w=1;
run;

data &amp;amp;outest;
     retain _TYPE_ &quot;PARMS&quot;  _NAME_ &quot;SCORE&quot;;
     array x{*} intercept &amp;amp;covars;
  do i=1 to dim(x); 
     x[i]=0;
  end;
  drop i;
  output;
run;

data _x&amp;amp;outest;
     retain _TYPE_ &quot;PARMS&quot;  _NAME_ &quot;SCORE&quot;;
     array bx{*} b_intercept &amp;amp;covars2;
  array x{*}  intercept &amp;amp;covars;
  set &amp;amp;outest;
  do j=1 to dim(x); bx[j]=x[j]; end;
  keep b_intercept &amp;amp;covars2 _TYPE_  _NAME_;
  drop j;
run;

sasfile _x&amp;amp;outest load;
%let stopiter=&amp;amp;maxiter;
%let status=Not Converged.;
%do i=1 %to &amp;amp;maxiter;
    %let t0=%sysfunc(datetime());

    %logistic(&amp;amp;dsn, &amp;amp;outest, &amp;amp;response, alpha=&amp;amp;alpha);
    %compare(&amp;amp;outest, _x&amp;amp;outest);    
    data &amp;amp;outest;
         retain _TYPE_ &quot;PARMS&quot;  _NAME_ &quot;SCORE&quot;;
         array bx{*} b_intercept &amp;amp;covars2;
      array x{*}  intercept &amp;amp;covars;
      set _x&amp;amp;outest;
      do j=1 to dim(x); x[j]=bx[j]; end;
      keep intercept &amp;amp;covars _TYPE_  _NAME_;
      drop j;
    run;
 %let alpha=%sysevalf(&amp;amp;alpha * &amp;amp;decay);
 %let alpha=%sysfunc(max(0.00005, &amp;amp;alpha));
    %let t1=%sysfunc(datetime());
    %let dt=%sysfunc(round(&amp;amp;t1-&amp;amp;t0, 0.001));
    %put Iteration &amp;amp;i, time used &amp;amp;dt, converge criteria is &amp;amp;maxdiff; 
 %if %sysevalf(&amp;amp;maxdiff&amp;lt;&amp;amp;criterion) %then %do;
     %let stopiter=&amp;amp;i;
        %let i=%eval(&amp;amp;maxiter+1);
  %let status=Converged.;
 %end;
%end;
sasfile _x&amp;amp;outest close;
%let t11=%sysfunc(datetime());
%let dt=%sysfunc(round(&amp;amp;t11-&amp;amp;t00, 0.01));
%put Total Time is &amp;amp;dt sec.;
%put Total Iteration is &amp;amp;stopiter, convergence status is &amp;amp;status;
%put At Final Iteration, max difference is &amp;amp;maxdiff;
options mlogic mprint notes source;
%mend;

&lt;/code&gt;&lt;/pre&gt;</content><link rel='replies' type='application/atom+xml' href='http://sas-programming.blogspot.com/feeds/2505374346425236390/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/29815492/2505374346425236390' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/2505374346425236390'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/29815492/posts/default/2505374346425236390'/><link rel='alternate' type='text/html' href='http://sas-programming.blogspot.com/2012/05/test-stochastic-gradient-decending.html' title='Stochastic Gradient Decending Logistic Regression in SAS'/><author><name>Liang Xie</name><uri>http://www.blogger.com/profile/02274752582289554390</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry></feed>