<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>The DO Loop</title>
	<atom:link href="https://blogs.sas.com/content/iml/feed" rel="self" type="application/rss+xml" />
	<link>https://blogs.sas.com/content/iml/</link>
	<description>Statistical programming in SAS with an emphasis on SAS/IML programs</description>
	<lastBuildDate>Mon, 10 Aug 2026 12:34:31 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.6</generator>
	<item>
		<title>Constructing orthogonal vectors in SAS</title>
		<link>https://blogs.sas.com/content/iml/2026/08/10/orthogonal-vectors.html</link>
					<comments>https://blogs.sas.com/content/iml/2026/08/10/orthogonal-vectors.html#respond</comments>
		
		<dc:creator><![CDATA[Rick Wicklin]]></dc:creator>
		<pubDate>Mon, 10 Aug 2026 09:29:54 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Matrix Computations]]></category>
		<category><![CDATA[Statistical Programming]]></category>
		<guid isPermaLink="false">https://blogs.sas.com/content/iml/?p=59712</guid>

					<description><![CDATA[<p>One of the great algorithms of linear algebra is the Gram-Schmidt orthogonalization process, which enables you to construct an orthogonal basis for a linear subspace from any set of linearly independent vectors that span the subspace. The Gram-Schmidt process is the basis for the QR decomposition in numerical linear algebra, [...]</p>
<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/08/10/orthogonal-vectors.html">Constructing orthogonal vectors in SAS</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>
One of the great algorithms of linear algebra is <a href="https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process">the Gram-Schmidt orthogonalization process</a>, which enables you to construct an orthogonal basis for a linear subspace from any set of linearly independent vectors that span the subspace.
The Gram-Schmidt process is the basis for the QR decomposition in numerical linear algebra, which decomposes any matrix, A, into a
product, A = QR, where Q is a matrix with orthonormal columns, and R is an upper triangular matrix.
In statistics, <a href="https://blogs.sas.com/content/iml/2021/07/12/qr-least-squares.html">the QR algorithm can be used for linear least-squares regression</a>, among other uses. 
</p><p>
The ideas behind the orthogonalization process are simple: Project a vector onto a linear subspace and compute the residual vector. The residual vector is orthogonal to the subspace. Thus, 
these two operations decompose any vector into two parts: A vector that lies in the subspace and a vector that is orthogonal to the subspace.
This article shows two subroutines in the SAS IML language that are useful for projecting a vector into a subspace and 
finding an orthogonal vector to a subspace. <a href="https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/imlug/imlug_langref_sect183.htm">The GSORTH subroutine</a> implements the <strong>G</strong>ram-<strong>S</strong>chmidt <strong>orth</strong>ogonalization of a matrix, whereas <a href="https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/imlug/imlug_langref_sect319.htm">the ORTVEC subroutine</a> performs one step
of the process and is useful for finding a <strong>orth</strong>ogonal <strong>vec</strong>tor to a subspace.
</p>

<h3>The Gram-Schmidt orthogonalization of a matrix</h3>
<p>
Suppose V1, V2, ..., Vk are k linearly independent vectors. Let S = span(V1, V2, ..., Vk) be the linear subspace that they span.
The Gram-Schmidt process uses these vectors to construct a set of
orthogonal vectors for S.  In SAS IML, you can put the vectors into the columns of a matrix and call the GSORTH subroutine.
The subroutine takes one input argument (a matrix, V) and returns three output matrices. The output are as follows:
</p>
<ol>
<li>Q: An orthonormal matrix whose columns contain an orthonormal basis for the subspace, S . </li>
<li>T: An upper triangular matrix that transforms the orthonormal basis into the original basis: V = Q*T. </li>
<li>lindep: A binary scalar value. If lindep=1, the columns of V are linearly dependent. If lindep=0, the columns of V are 
linearly independent, which implies dim(S) = k. </li>
</ol>

<p>Here's an example. The columns of V span a 3-D linear subspace in the R<sup>4</sup> Euclidean space.
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #000080; font-weight: bold;">proc iml</span>;
<span style="color: #006400; font-style: italic;">/* the columns of V span a 3-D subspace in R^4 */</span>
V = <span style="color: #66cc66;">&#123;</span> <span style="color: #2e8b57; font-weight: bold;">1</span> <span style="color: #2e8b57; font-weight: bold;">1</span>  <span style="color: #2e8b57; font-weight: bold;">0</span>,
     -<span style="color: #2e8b57; font-weight: bold;">1</span> <span style="color: #2e8b57; font-weight: bold;">0</span>  <span style="color: #2e8b57; font-weight: bold;">2</span>,
     -<span style="color: #2e8b57; font-weight: bold;">1</span> <span style="color: #2e8b57; font-weight: bold;">1</span> -<span style="color: #2e8b57; font-weight: bold;">1</span>,
      <span style="color: #2e8b57; font-weight: bold;">1</span> <span style="color: #2e8b57; font-weight: bold;">1</span>  <span style="color: #2e8b57; font-weight: bold;">2</span> <span style="color: #66cc66;">&#125;</span>;
&nbsp;
<span style="color: #0000ff;">call</span> gsorth<span style="color: #66cc66;">&#40;</span>Q, T, lindep, V<span style="color: #66cc66;">&#41;</span>;  <span style="color: #006400; font-style: italic;">/* output: Q, T, and lindep from the input matrix, V */</span>
print Q<span style="color: #66cc66;">&#91;</span>F=BestD6.<span style="color: #66cc66;">&#93;</span>, T<span style="color: #66cc66;">&#91;</span>F=BestD6.<span style="color: #66cc66;">&#93;</span>;
<span style="color: #006400; font-style: italic;">/* verify that V = Q*T by computing the difference V - Q*T */</span>
maxDiff = <span style="color: #0000ff;">max</span><span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">abs</span><span style="color: #66cc66;">&#40;</span>V - Q<span style="color: #006400; font-style: italic;">*T));</span>
print maxDiff;</pre></td></tr></table></div>




<img fetchpriority="high" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/08/ortvec1.png" alt="" width="154" height="341" class="alignnone size-full wp-image-59754" srcset="https://blogs.sas.com/content/iml/files/2026/08/ortvec1.png 154w, https://blogs.sas.com/content/iml/files/2026/08/ortvec1-135x300.png 135w" sizes="(max-width: 154px) 100vw, 154px" />

<p>
The original three basis vectors are V1={1, -1, -1, 1}, V2={1, 0, 1, 1}, and V3={0, 2, -1, 2}.
They span a subspace, S, in R<sup>4</sup>.
After calling the GSORTH routine, the columns of the matrix Q contain an orthogonal set of 
basis vectors that also span S.  (To standardize the output, the columns of Q have unit norm, 
which means they form an orthonormal basis.)
The T matrix maps one set of basis vectors onto the other. 
Specifically, V = Q*T is the equation that represents each column of V as a linear combination of the columns of Q.
For this example, the columns of V are linearly independent, so lindep=0 and dim(S) = 3.
</p>
<p>
Notice that this Q matrix is not square, so it cannot be called an <a href="https://en.wikipedia.org/wiki/Orthogonal_matrix">orthogonal matrix</a>. 
Instead, a rectangular matrix with orthonormal columns is called a
<a href="https://en.wikipedia.org/wiki/Orthogonal_matrix#Rectangular_matrices">semi-orthogonal matrix</a>.
If, in addition, the columns have unit length, it is called a semi-orthonormal matrix.
</p>

<h3>Computing orthogonal vectors to a linear subspace: The manual way</h3>
<p>
The GSORTH subroutine enables you to compute an orthonormal basis with a single call. In linear algebra texts, the Q matrix
is usually constructed sequentially. At each step, you create a new orthonormal basis vector from one of the original spanning vectors.
To understand how the GSORTH and ORTVEC subroutines work, it helps to look at the manual calculations. 
In my linear algebra class, the Gram-Schmidt orthogonalization process was used so often 
that it is indelibly etched into my brain from performing endless calculations by hand!
This section shows the tedious manual computations. The subsequent section uses the ORTVEC subroutine to simplify these computations.
</p>
<p>
The Gram-Schmidt process builds an orthogonal basis incrementally. Let's look at the SAS IML code.
The original vectors are V<sub>1</sub>, V<sub>2</sub>, and V<sub>3</sub>, which are columns of a matrix, V. 
We use them to construct a new set of orthogonal vectors (U<sub>1</sub>, U<sub>2</sub>, and U<sub>3</sub>), 
which are columns of a semi-orthogonal matrix. 
</p>
<p>
The process works step-by-step, projecting vectors onto the existing subspace and calculating the residual:
</p>

<ul>

  <li>
    <b>Step 1:</b> The first orthogonal vector, U<sub>1</sub>,  is simply the first original vector. 
<br />
    U<sub>1</sub> = V<sub>1</sub>
<br />

<a href="https://blogs.sas.com/content/iml/files/2026/08/ortvec4.png"><img decoding="async" src="https://blogs.sas.com/content/iml/files/2026/08/ortvec4.png" alt="" width="200" height="118" class="alignright size-full wp-image-59802" /></a>

If you want to, you can standardize U<sub>1</sub> construct an orthonormal basis, but let's keep it simple for now.
We'll standardize the U vectors at the end.
  </li>

  <li>
    <b>Step 2:</b> The second orthogonal vector, U<sub>2</sub>, is the second original vector minus its projection onto the first orthogonal vector. Geometrically, you decompose V<sub>2</sub> into two components, one lying in the span of U<sub>1</sub> and the other orthogonal to it.
The orthogonal component becomes U<sub>2</sub>:
    <br />
    U<sub>2</sub> = V<sub>2</sub> - proj(V<sub>2</sub>, U<sub>1</sub>)
  </li>

<a href="https://blogs.sas.com/content/iml/files/2026/08/ortvec5.png"><img decoding="async" src="https://blogs.sas.com/content/iml/files/2026/08/ortvec5.png" alt="" width="350" height="120" class="alignright size-full wp-image-59799" /></a>

  <li>
    <b>Step 3:</b> The third orthogonal vector is the third original vector minus its projections onto the first two orthogonal vectors.
Geometrically, you decompose V<sub>3</sub> into two components, one lying in span(U<sub>1</sub>, U<sub>2</sub>) and the other orthogonal to it.
The orthogonal component becomes U<sub>3</sub>:
    <br />
    U<sub>3</sub> = V<sub>3</sub> - proj(V<sub>3</sub>, U<sub>1</sub>) - proj(V<sub>3</sub>, U<sub>2</sub>)
  </li>
</ul>

<p>
In these equations, the projection operator <code class="preserve-code-formatting">proj(y, x)</code> calculates the projection of a vector <em>y</em> onto the line spanned by vector <em>x</em>. 
Let's implement these three steps of the G-S orthogonalization process in the SAS IML language.
To make sure we get the same answer as the GSORTH subroutine, we can standardize the columns of U to form Q:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* manual operation: project y onto span(x) */</span>
start proj_vec<span style="color: #66cc66;">&#40;</span>y, <span style="color: #0000ff;">x</span><span style="color: #66cc66;">&#41;</span>;
   z = <span style="color: #0000ff;">x</span>/ norm<span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">x</span><span style="color: #66cc66;">&#41;</span>;        <span style="color: #006400; font-style: italic;">/* unit vector in direction of x */</span>
   <span style="color: #0000ff;">return</span><span style="color: #66cc66;">&#40;</span> <span style="color: #66cc66;">&#40;</span>y`<span style="color: #006400; font-style: italic;">*z) * z );</span>
finish;
&nbsp;
U = j<span style="color: #66cc66;">&#40;</span>nrow<span style="color: #66cc66;">&#40;</span>V<span style="color: #66cc66;">&#41;</span>, ncol<span style="color: #66cc66;">&#40;</span>V<span style="color: #66cc66;">&#41;</span>, .<span style="color: #66cc66;">&#41;</span>;
U<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#93;</span> = V<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#93;</span>;
U<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#93;</span> = V<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#93;</span> - proj_vec<span style="color: #66cc66;">&#40;</span>V<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#93;</span>, U<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#93;</span><span style="color: #66cc66;">&#41;</span>;
U<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">3</span><span style="color: #66cc66;">&#93;</span> = V<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">3</span><span style="color: #66cc66;">&#93;</span> - proj_vec<span style="color: #66cc66;">&#40;</span>V<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">3</span><span style="color: #66cc66;">&#93;</span>, U<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#93;</span><span style="color: #66cc66;">&#41;</span> - proj_vec<span style="color: #66cc66;">&#40;</span>V<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">3</span><span style="color: #66cc66;">&#93;</span>, U<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#93;</span><span style="color: #66cc66;">&#41;</span>;
print U;
&nbsp;
<span style="color: #006400; font-style: italic;">/* optionally normalize the columns of U at each step or at the end. */</span>
norm_vec = j<span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">1</span>, ncol<span style="color: #66cc66;">&#40;</span>U<span style="color: #66cc66;">&#41;</span>, .<span style="color: #66cc66;">&#41;</span>;
Q = j<span style="color: #66cc66;">&#40;</span>nrow<span style="color: #66cc66;">&#40;</span>U<span style="color: #66cc66;">&#41;</span>, ncol<span style="color: #66cc66;">&#40;</span>U<span style="color: #66cc66;">&#41;</span>, .<span style="color: #66cc66;">&#41;</span>;
<span style="color: #0000ff;">do</span> j = <span style="color: #2e8b57; font-weight: bold;">1</span> to ncol<span style="color: #66cc66;">&#40;</span>U<span style="color: #66cc66;">&#41;</span>;
   norm_vec<span style="color: #66cc66;">&#91;</span>j<span style="color: #66cc66;">&#93;</span> = norm<span style="color: #66cc66;">&#40;</span>U<span style="color: #66cc66;">&#91;</span>,j<span style="color: #66cc66;">&#93;</span><span style="color: #66cc66;">&#41;</span>;
   Q<span style="color: #66cc66;">&#91;</span>,j<span style="color: #66cc66;">&#93;</span> = U<span style="color: #66cc66;">&#91;</span>,j<span style="color: #66cc66;">&#93;</span> / norm_vec<span style="color: #66cc66;">&#91;</span>j<span style="color: #66cc66;">&#93;</span>;
<span style="color: #0000ff;">end</span>;
print norm_vec<span style="color: #66cc66;">&#91;</span>F=Best5.<span style="color: #66cc66;">&#93;</span>, Q<span style="color: #66cc66;">&#91;</span>F=BestD6.<span style="color: #66cc66;">&#93;</span>;</pre></td></tr></table></div>




<img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/08/ortvec3.png" alt="" width="143" height="367" class="alignnone size-full wp-image-59787" srcset="https://blogs.sas.com/content/iml/files/2026/08/ortvec3.png 143w, https://blogs.sas.com/content/iml/files/2026/08/ortvec3-117x300.png 117w" sizes="(max-width: 143px) 100vw, 143px" />

<p>
Notice that the Q matrix from these steps is identical to the Q matrix that is produced by the GSORTH subroutine.
The U matrix is an "un-standardized" version of Q.
</p>

<h3>Computing orthogonal vectors to a linear subspace: The easier way</h3>
<p>
As described in the previous section, the Gram-Schmidt process is a series of operations that projects vectors onto a linear subspace, 
computes a vector orthogonal to the subspace, and then iterates. 
<a href="https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/imlug/imlug_langref_sect319.htm">The ORTVEC subroutine</a> 
encapsulates one step in this iterative process.
It standardizes the orthogonal vectors at each step, thus forming the Q matrix directly instead of first forming U and then standardizing the columns.
</p><p>
The syntax for the ORTVEC routine is a little complicated. Let <em>n</em> be the dimension of the vectors (in our example, n=4).
The input arguments for the ORTVEC subroutine are an <em>n</em>&nbsp;x&nbsp;1 vector, v, and a semi-orthonormal <em>n</em>&nbsp;x&nbsp;<em>k</em> matrix, Q, where <em>k</em> &le; <em>n</em>.
This syntax is used for finding the (k+1)th orthonormal basis vector, based on the previous k orthonormal vectors.
The vector v is the (k+1)th vector in the original basis.
The ORTVEC routine returns four values: two vectors, w and r, a scalar rho, and a binary flag that tells you whether v is in the span of the columns of Q. The return values provide the decomposition of v into a component inside span(Q) and a component orthogonal to span(Q). In symbols,
v = Q*r + rho*w, where norm(w)=1.
The vector w is orthogonal to the span of the columns of Q.
The vector w is the most important output from the ORTVEC subroutine. 
</p>
<p>
To get the first orthonormal basis vector, you omit the Q matrix.
This is shown in the following example, which uses the ORTVEC subroutine to compute the same Q vector as in the previous section:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* the ORTVEC function in SAS IML performs several linear algebra operations
   related to G-S orthogonalization */</span>
Q= j<span style="color: #66cc66;">&#40;</span>nrow<span style="color: #66cc66;">&#40;</span>V<span style="color: #66cc66;">&#41;</span>, ncol<span style="color: #66cc66;">&#40;</span>V<span style="color: #66cc66;">&#41;</span>, .<span style="color: #66cc66;">&#41;</span>;
<span style="color: #0000ff;">call</span> ortvec<span style="color: #66cc66;">&#40;</span>w1,r,rho1,lindep, V<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#93;</span><span style="color: #66cc66;">&#41;</span>;  <span style="color: #006400; font-style: italic;">/* Step 1: No Q matrix yet; rho = norm(v1); v1 = rho1*w1 */</span>
Q<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#93;</span> = w1;
<span style="color: #006400; font-style: italic;">/* Check: v1 = rho1*w1; print (v1-V[,1]); */</span>
&nbsp;
<span style="color: #0000ff;">call</span> ortvec<span style="color: #66cc66;">&#40;</span>w2,r2,rho2,lindep, V<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#93;</span>, Q<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#93;</span><span style="color: #66cc66;">&#41;</span>; <span style="color: #006400; font-style: italic;">/* Step 2: v2 = Q1*r2 + rho2*w2 */</span>
Q<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#93;</span> = w2;
<span style="color: #006400; font-style: italic;">/* Check: v2 = Q[,1]*r2 + rho2*w2; print (v2-V[,2]); */</span>
&nbsp;
<span style="color: #0000ff;">call</span> ortvec<span style="color: #66cc66;">&#40;</span>w3,r3,rho3,lindep, V<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">3</span><span style="color: #66cc66;">&#93;</span>, Q<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">1</span>:<span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#93;</span><span style="color: #66cc66;">&#41;</span>; <span style="color: #006400; font-style: italic;">/* Step 3: v3 = [Q1 Q2]*r3 + rho3*w3 */</span>
Q<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">3</span><span style="color: #66cc66;">&#93;</span> = w3;
<span style="color: #006400; font-style: italic;">/* Check: v3 = Q[,1:2]*r3 + rho3*w3; print (v3 - V[,3]); */</span>
print Q;</pre></td></tr></table></div>




<img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/08/ortvec2.png" alt="" width="149" height="149" class="alignnone size-full wp-image-59769" />

<p>
You now have three equivalent ways to construct the Q matrix. 
If you are interested in a programming challenge, you could put these ORTVEC operations into a DO loop (or a function) that performs 
the entire Gram-Schmidt orthogonalization algorithm
for an arbitrary matrix, V. 
For an extra challenge, print out the sequence of 'r' vectors and the 'rho' scalars. 
Can you determine how they are related to the matrix T that is provided by the GSORTH routine?
</p>

<h3>Summary</h3>
<p>
The Gram-Schmidt orthogonalization algorithm enables you to construct an orthogonal basis for the span of other linearly independent vectors.
The orthogonalization process decomposes every vector into 
a component that lies inside a subspace and a vector that is orthogonal to the subspace.
In the SAS IML language, you can use the GSORTH subroutine to perform the full algorithm, or you can use 
the ORTVEC subroutine to perform one step.
There are many applications that require finding a vector that is orthogonal to a subspace, so 
the ORTVEC subroutine is useful in these applications.
</p>
<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/08/10/orthogonal-vectors.html">Constructing orthogonal vectors in SAS</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blogs.sas.com/content/iml/2026/08/10/orthogonal-vectors.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<enclosure url="https://blogs.sas.com/content/iml/files/2020/12/ProjRSq1-150x150.png" />
	</item>
		<item>
		<title>Nonnegative matrix factorization with sparseness constraints</title>
		<link>https://blogs.sas.com/content/iml/2026/08/03/nmf-sparseness.html</link>
					<comments>https://blogs.sas.com/content/iml/2026/08/03/nmf-sparseness.html#respond</comments>
		
		<dc:creator><![CDATA[Rick Wicklin]]></dc:creator>
		<pubDate>Mon, 03 Aug 2026 09:21:23 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Data Analysis]]></category>
		<category><![CDATA[Matrix Computations]]></category>
		<guid isPermaLink="false">https://blogs.sas.com/content/iml/?p=59601</guid>

					<description><![CDATA[<p>A previous article describes the nonnegative matrix factorization (NMF). You can use the NMF to reveal important features in data that can be used to reduce the dimensionality of the problem. NMF is useful when the data are nonnegative, such as counts or pixel values in an image. The goal [...]</p>
<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/08/03/nmf-sparseness.html">Nonnegative matrix factorization with sparseness constraints</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>
A previous article describes <a href="https://blogs.sas.com/content/iml/2026/03/30/nmf-whisky.html">the nonnegative matrix factorization (NMF)</a>. 
You can use the NMF to reveal important features in 
data that can be used to reduce the dimensionality of the problem. NMF is useful when the data are nonnegative, such as counts or pixel values in an image.
The goal of NMF is to find a small number of new nonnegative variables that are interpretable and such that 
each original observation is approximated as a nonnegative linear combination of these new variables. In that sense, NMF shares similarities with principal component analysis (PCA), but the PCA factors and the coefficients contain negative values, which hampers interpretability.
</p>
<p>
Recently, I read a paper by Patrick Hoyer (<a href="https://www.jmlr.org/papers/volume5/hoyer04a/hoyer04a.pdf">2004, "Non-negative matrix factorization with sparseness constraints"</a>), that imposes constraints on the NMF method. The constraints are designed to make the factors more sparse, which is why Hoyer introduced his <a href="https://blogs.sas.com/content/iml/2026/07/27/hoyer-sparseness.html">sparseness measure for vectors</a>. Briefly, Hoyer's algorithm results in nonnegative factors that might be more interpretable. 
</p><p>
This article shows an example by revisiting the Scotch whisky data, which I analyzed by using NMF in a previous article.
On paper, Hoyer's idea seemed both interesting and powerful. However, after running a sparseness-constrained NMF on an example, I was not 
impressed.
Read on to find out why. 
To try out the Hoyer algorithm yourself, 
<a href="https://github.com/sascommunities/the-do-loop-blog/blob/master/NMF/NMF_sparse.sas">download the SAS program that 
implements a sparseness-constrained NMF factorization</a>.

</p>

<h3>An unconstrained feature set for Scotch whiskies</h3>
<p>
<a href="https://blogs.sas.com/content/iml/2026/03/30/nmf-whisky.html">The previous article</a> 
used the NMF to extract a four-dimensional basis of vectors for Scotch whisky data.
It is known that whiskies often have regional characteristics, such as the
"earthy" or "peaty" whiskies from the island of Islay. Ideally, the NMF features would reflect these regional flavor 
characteristics.
</p><p>
The whisky data can be represented as an
86x12 nonnegative data matrix, X.
Each row represents flavor characteristics of a whisky.
Each whisky is assigned a flavor profile that consists of the values 0-4 for the 12 flavors.
The 12 flavors are Tobacco, Medicinal, Smoky, Body, Spicy, Winey, Nutty, Honey, Malty, Fruity, Sweetness, and Floral.

</p><p>
The NMF creates a rank-k approximation to X. This article uses k=4.
Thus, X is approximated as a product W*H,
where W is 86x4 and H is 4x12.
The rows of H are the "flavor profile" feature vectors.
They form a basis for a 4-D linear subspace.
They are analogous to the principal components (eigenvectors) 
in a PCA analysis.
Each row of the W matrix is a set of weights (or coefficients) for the 4-factor approximation of the corresponding row of X. 
</p><p>
The following image shows the transposed H matrix. The columns in the image visualize a 4-D basis for the unconstrained NMF. 
</p>

<a href="https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse1.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse1.png" alt="" width="360" height="480" class="alignnone size-full wp-image-59649" srcset="https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse1.png 360w, https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse1-225x300.png 225w" sizes="(max-width: 360px) 100vw, 360px" /></a>


<p>The columns of H` are the features found by the NMF. The NMF method is not unique, but in this image:
</p>
<ol>
<li>The first NMF factor is a combination of six flavors: Smoky, Spicy, Malty, Fruity, Sweetness, and Floral variables.
</li>
<li>The second NMF factor is a combination of 10 flavors. 
</li>
<li>The third factor is primarily composed of the Tobacco, Medicinal, Smoky, and Body flavors.
</li>
<li>The fourth factor is primarily composed of Nutty flavor, with smaller amounts of Malty and Sweetness.
</li>
</ol>

<p>
In terms of <a href="https://blogs.sas.com/content/iml/2026/07/27/hoyer-sparseness.html">Hoyer's sparseness measure</a>, the first and second features are not sparse. Hoyer's measure ranges from 0 (not at all sparse) to 1 (very sparse). The following table shows that 
the first NMF feature has moderate sparseness (0.459). 
The second feature has very little sparseness (0.148). 
The third and fourth features are strongly sparse (0.601) and very strongly sparse (0.822), respectively.
</p>

<img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse2.png" alt="" width="215" height="66" class="alignnone size-full wp-image-59646" />

<p>
Before leaving the visualization of the unconstrained NMF, I want to draw your attention to the first row of H`. The third factor contains a small amount of the Tobacco variable. The cell is light orange. If you print out the H matrix, you can see that the first and second factor also contain a trace amount of the tobacco variable. However, compared to the other variables, this rank-4 NMF approximation does not represent the tobacco variable well. It seems that the tobacco variable probably points in a direction that is primarily orthogonal to the span of these four NMF factors.
</p>

<h3>A sparseness-constrained NMF algorithm</h3>
<p>
There are 12 variables and k=4 factors, so it would be nice if features were combinations of no more than four or five variables. 
If a 12-D vector has 4 nonzero components and 8 zero components, Hoyer's sparseness measure is about 0.6.
Let's use that value as a target value. 
What will the value 0.6 do?
The unconstrained factors naturally ranged from 0.15 to 0.82. 
The sparseness constraint forces the dense factor (0.15) to become much sparser.
However, it will actually <em>reduce</em> the sparseness of the fourth factor!
</p><p>

I implemented Hoyer's sparseness-constrained NMF in SAS IML. The following image shows the result of running this constrained NMF algorithm on the Whisky data and constraining the rows of the H matrix to have sparseness equal to 0.6:
</p>


<a href="https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse3.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse3.png" alt="" width="360" height="480" class="alignnone size-full wp-image-59643" srcset="https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse3.png 360w, https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse3-225x300.png 225w" sizes="(max-width: 360px) 100vw, 360px" /></a>


<p>
In the new NMF factorization, the sparseness of EVERY feature (rows of the H matrix) is exactly 0.6. 
In the new factorization, all features depend on 4-5 variables. 
Notice that the second feature, which used to be a blend of 10 flavors, is very different. 
Some of the flavors that were previously captured in the second feature are now part of other factors. 
For example, the Winey contributions have moved to the fourth factor,
and the Honey contribution has moved to the first factor.
</p><p>
If you look closely at the first row in the image, you'll see that 
no factor contains the Tobacco variable!
In this rank-4 sparseness-constrained factorization, the sparseness constraint forces the columns of H` to 
be formed by using only a few variables. The Tobacco variable is not included in any sparse factor.
Thus, this model predicts that no Scotch whisky has a Tobacco flavor. 
</p>
<p>
If you want to include
the Tobacco variable in the NMF factors, you need to relax
the sparseness constraint (or use a higher-rank approximation). For example, setting the sparseness target
to 0.5 will result in factors that include Tobacco, but the resulting factors each contain six or more variables
and are not as interpretable.
</p><p>
This is a specific instance of a general truth: The unconstrained NMF factorization will always 
approximate the data matrix better than a sparseness-constrained factorization. 
Sometimes, the lack of fit might be qualitatively noticeable, as in this example
where the Tobacco variable is not included in any NMF factor.
</p>

<h3>The tradeoffs between sparseness and goodness of fit</h3>
<p>
The sparseness-constrained NMF is always a poorer approximation to the data matrix. Thus, you have to decide
whether the trade-offs are worth it: 
</p>
<ul>
<li>Is it better to use the unconstrained NMF? The fit is better, but some factors might 
be formed as a linear combination of many variables, which hampers interpretability. 
</li>
<li>Is it better to use the sparseness-constrained NMF? The fit is worse, but you can force the factors 
to be sparse and therefore highly interpretable. On the other hand, the sparseness constrained NMF
is a <a href="https://en.wikipedia.org/wiki/Procrustes">Procrustean solution</a> to the interpretability
of the factors. By forcing every factor to have the same sparseness, it ignores 
the natural data-induced correlations between variables. 
</li>
</ul>
<p>
My general preference is to let the data decide how many variables are
contained in each NMF factor. The unconstrained NMF approximation seems more faithful to the data.
The factors in the unconstrained NMF are mostly interpretable. Their sparseness ranges from 
0.148 (not sparse) to 0.822 (strongly sparse).
It isn't terrible to have a factor whose 
interpretation is "an overall blend of flavors." After all, every observation is a Scotch whisky,
so it isn't surprising to have a common factor that describes flavors that are common to many whiskies.
</p><p>
In the sparseness-constrained algorithm, every factor has exactly the same sparseness.
Every factor depends strongly on four or five flavors.
This does not seem realistic. 
If regional distilleries produce whiskies that have one or two special flavor characteristics, 
it seems better to have factors that are linear combinations of only one or two flavors.
But the sparseness of such a factor would be too high for the specified constraint.
</p>

<h3>Summary</h3>
<p>
In multivariate statistics, the best algorithms are often those that let the data decide the features.
By adding a sparseness constraint to the NMF algorithm,
we let <a href="https://www.merriam-webster.com/dictionary/the%20tail%20wagging%20the%20dog">the tail wag the dog</a>.
The sparseness essentially dictates how many variables are important in each factor.
If the sparseness value is high, then every factor will contain a small number of important variables,
which might not be appropriate for the data. 
</p><p>
There is also the mathematical fact that a constrained solution will always have a poorer fit than an unconstrained solution.
By itself, that doesn't bother me too much, but you should get something valuable for the loss of fit. 
In a sparseness-constrained NMF, it isn't clear that the benefits offset the poorer approximation to the data.
</p>


<h3>Appendix: Sparseness and goodness of fit</h3>
<p>
It is an interesting exercise to plot the fit of rank-4 NMF approximation as you let the sparseness constraint vary from small to large values.
The following image summarizes that experiment for the Whisky data.
I have overlaid a few plots of the H` matrix so that you can see the effect that sparseness has on the NMF factors.
The horizontal line is the relative error of the unconstrained NMF.  The relative error 
compares the NMF error E=||X - W*H||<sub>F</sub> with the smallest possible error, which is produced by using a rank-k SVD.
</p>

<a href="https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse4.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse4.png" alt="" width="653" height="415" class="alignnone size-full wp-image-59679" srcset="https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse4.png 1088w, https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse4-300x191.png 300w, https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse4-1024x651.png 1024w, https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse4-768x488.png 768w" sizes="(max-width: 653px) 100vw, 653px" /></a>
<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/08/03/nmf-sparseness.html">Nonnegative matrix factorization with sparseness constraints</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blogs.sas.com/content/iml/2026/08/03/nmf-sparseness.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<enclosure url="https://blogs.sas.com/content/iml/files/2026/08/NMF_sparse3-150x150.png" />
	</item>
		<item>
		<title>Hoyer&#039;s sparseness measure</title>
		<link>https://blogs.sas.com/content/iml/2026/07/27/hoyer-sparseness.html</link>
					<comments>https://blogs.sas.com/content/iml/2026/07/27/hoyer-sparseness.html#comments</comments>
		
		<dc:creator><![CDATA[Rick Wicklin]]></dc:creator>
		<pubDate>Mon, 27 Jul 2026 09:26:57 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Data Analysis]]></category>
		<category><![CDATA[Matrix Computations]]></category>
		<guid isPermaLink="false">https://blogs.sas.com/content/iml/?p=59489</guid>

					<description><![CDATA[<p>In his 2004 paper, "Non-negative Matrix Factorization with Sparseness Constraints," Patrick Hoyer introduced a function that measures the sparseness of a nonzero vector. The paper does not explain or motivate the formula, so this article describes the geometry and intuition behind Hoyer's formula, along with a visualization and examples. Hoyer's [...]</p>
<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/07/27/hoyer-sparseness.html">Hoyer&#039;s sparseness measure</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>
In his 2004 paper, <a href="https://www.jmlr.org/papers/volume5/hoyer04a/hoyer04a.pdf">"Non-negative Matrix Factorization with Sparseness Constraints,"</a> Patrick Hoyer introduced a function that measures the sparseness of a nonzero vector.
The paper does not explain or motivate the formula, so this article 
describes the geometry and intuition behind Hoyer's formula, along with a visualization and examples.
Hoyer's sparseness measure has been used to analyze matrix factorizations and to interpret spectra in mass spectrometry.
</p>

<h3>Sparseness versus sparsity</h3>
<p>
First, I want to clarify the difference between Hoyer's definition of sparseness (for a vector) and the traditional
notion of sparsity (for a matrix).
</p><p>
In numerical analysis, <a href="https://blogs.sas.com/content/iml/2020/06/22/visualize-structure-sparse-matrix.html">a matrix is said to be <em>sparse</em> if most of its elements are exactly zero</a> and only a few elements are nonzero. For example, a large 
diagonal (or tridiagonal matrix) is sparse because the only nonzero elements are on (or near) the diagonal, and 
other elements are exactly zero. The sparsity of a matrix is reported as the proportion of elements that are zero.
For an <em>n</em>&nbsp;x&nbsp;<em>m</em>, this is  card(A[i,j] = 0) / (<em>n*m</em>), which is a number in [0,1].
Very sparse matrices have sparsity close to 1, whereas very dense matrices have sparsity close to 0.
An <em>n</em>&nbsp;x&nbsp;<em>n</em> diagonal matrix has sparsity 1 &ndash; 1/<em>n</em>.
</p>

<p>
Hoyer's measure of sparseness is for vectors, not matrices. 
It does not count zeros and non-zeros. Rather, it looks at the magnitudes of the elements and measures how many are relatively large and how many are 
relatively small. If all elements are approximately the same magnitude, the vector has a sparseness measure that is close to 0. If there are a small number of large elements and the other elements are small, the vector has a measure that is close to 1.
</p>


<h3>Hoyer's sparseness measure</h3>
<p>
Hoyer's sparseness measure is defined as follows. If <strong>v</strong> &ne; <strong>0</strong> is an <em>n</em>-dimensional vector, <em>n</em> &ge; 2, the 
sparseness of <strong>v</strong> is defined as
<br /> 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<span class='MathJax_Preview'>\(S(v) = \frac{\sqrt{n} - \|v\|_1 / \|v\|_2}{\sqrt{n} - 1}\)</span><script type='math/tex'>S(v) = \frac{\sqrt{n} - \|v\|_1 / \|v\|_2}{\sqrt{n} - 1}</script>
<br /> 
where
<span class='MathJax_Preview'>\(\|v\|_1 = \sum_{i=1}^n |v_i|\)</span><script type='math/tex'>\|v\|_1 = \sum_{i=1}^n |v_i|</script> is the L<sub>1</sub> vector norm, and 
<span class='MathJax_Preview'>\(\|v\|_2 = \sqrt{ \sum_{i=1}^n v_i^2 }\)</span><script type='math/tex'>\|v\|_2 = \sqrt{ \sum_{i=1}^n v_i^2 }</script> is the L<sub>2</sub> or Euclidean norm.
</p>
<p>
Clearly, the ratio of the L<sub>1</sub> norm to the L<sub>2</sub> norm is an important quantity. There are a few easy mathematical results about the ratio <span class='MathJax_Preview'>\(\|v\|_1 / \|v\|_2\)</span><script type='math/tex'>\|v\|_1 / \|v\|_2</script>:
</p>
<ol>
<li>The ratio is invariant under a change of signs for any component. That is, if S is a diagonal matrix whose diagonal elements are &plusmn;1, 
then <span class='MathJax_Preview'>\(\|S v\| = \|v\|\)</span><script type='math/tex'>\|S v\| = \|v\|</script>.
</li>
<li>The ratio is scale invariant. For any <em>c</em> &ne; 0, scalar multiplication by <em>c</em> does not change the ratio: <span class='MathJax_Preview'>\(\|c v\|_1 / \|c v\|_2 =  \|v\|_1 / \|v\|_2\)</span><script type='math/tex'>\|c v\|_1 / \|c v\|_2 =  \|v\|_1 / \|v\|_2</script>.
</li>
<li>The ratio is permutation invariant because the norms are permutation invariant. That is, if you permute the coordinates of <strong>v</strong>, you do not change the norm or the ratio of norms. In terms of matrices, if P is any permutation matrix,  <span class='MathJax_Preview'>\(\|P v\| = \|v\|\)</span><script type='math/tex'>\|P v\| = \|v\|</script>.
</li>
</ol>

<p>
The first property tell you that it suffices to understand the ratio of norms for "positive vectors" in the first octant (that is, all components are positive). The second property tells you that you can standardize the vectors to have unit L<sub>2</sub> norm.
The third property tells you that you can assume that |v<sub>1</sub>| &ge; |v<sub>2</sub>| &ge; &hellip; &ge; |v<sub>2</sub>|, if you want to.
These properties are apparent in the subsequent visualizations.
</p>


<h3>The range of the Hoyer measure</h3>
<p>
For any nonzero <em>n</em>-dimensional vector, the Hoyer measure of sparseness is in the interval [0, 1].
</p>
<p>
The Hoyer measure achieves its maximum value of 1 when (<em>n</em>-1) components of <strong>v</strong> are zero and the remaining component is nonzero. 
The properties in the previous section enable us to assume, without loss of generality, that the first component is nonzero. Then,
||v||<sub>1</sub> = ||v||<sub>2</sub>, so the ratio of norms is 1. It follows that S(<strong>v</strong>) = 1 when <strong>v</strong> has only one non-zero component.
</p>
<p>
The Hoyer measure achieves its minimum value of 0 when all components of <strong>v</strong> are equal. 
The properties in the previous section enable us to standardize the vector so that its Euclidean norm is 1.
If all components are equal, then ||<strong>v</strong>||<sub>2</sub> = sqrt(&Sigma; <em>a</em><sup>2</sup>) = 1 for some value, <em>a</em>.
Thus, <em>a</em> = &plusmn;1/sqrt(<em>n</em>). Consequently, ||<strong>v</strong>||<sub>1</sub> = <em>n</em> / sqrt(<em>n</em>) = sqrt(<em>n</em>), so the numerator of Hoyer's measure vanishes. It follows that S(<strong>v</strong>) = 0 when all components of <strong>v</strong> are equal.
</p>

<h3>Examples of Hoyer's sparseness</h3>
<a href="https://blogs.sas.com/content/iml/files/2026/07/sparseness5.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/07/sparseness5.png" alt="" width="300" height="400" class="alignright size-full wp-image-59538" srcset="https://blogs.sas.com/content/iml/files/2026/07/sparseness5.png 300w, https://blogs.sas.com/content/iml/files/2026/07/sparseness5-225x300.png 225w" sizes="(max-width: 300px) 100vw, 300px" /></a>

<p>
Let's illustrate this concept with an example. Consider the following 6-D vectors. To within four decimal places, the Euclidean (or L2) norm of each vector is 1.
</p>
<ul>
<li><strong>q</strong> = {0.4082, 0.4082, 0.4082, 0.4082, 0.4082, 0.4082}. The L1 norm is 2.4492, and the Hoyer sparseness is 0.
</li>
<li><strong>s</strong> = {0.5774, 0.5774, 0.5774, 0, 0, 0}. The L1 norm is 1.7322, and the Hoyer sparseness is 0.4950.
</li>
<li><strong>u</strong> = {0.9948, 0.08, 0.04, 0.04, 0.02, 0.02}. The L1 norm is 1.1948, and the Hoyer sparseness is 0.8656. Notice that the sparseness is high even though no element is exactly 0.
</li>
</ul>

<p>
The panel of graphs to the right visualizes the meaning of Hoyer's sparseness measure. Vectors that have a few elements that are larger than the other have the most sparseness. Vectors whose components are approximately uniformly distributed have the least sparseness.
</p>


<h3>Visualize Hoyer's sparseness for 2-D vectors</h3>
<p>
Since Hoyer's measure is invariant under scaling, it suffices to visualize the measure for unit vectors.
If the vectors are two-dimensional, the unit vectors are on the unit circle centered at the origin.
From the previous section, we know that the measure will obtain its maximum value of 1 for vectors that are aligned with a coordinate axis.
It will obtain its minimum value of 0 when the components are equal in magnitude.
In the first quadrant, this occurs when the vector makes an angle of 45 degrees or &pi;/4 radians. By the permutation invariance, there
are also minima for vectors at 3&pi;/4, 5&pi;/4, and 7&pi;/4 radians. 
</p>
<p>
The following SAS DATA step visualizes the Hoyer sparseness measure for 2-D vectors on the unit circle:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* A measure of sparsity of a vector from:
   Patrick O. Hoyer. Non-negative matrix factorization with sparseness constraints. 
                     Journal of Machine Learning Research, 5:1457–1469, 2004.
   The &quot;most sparse&quot; vector is one for which only a single component is non-zero.
   That is, it aligns with a coordinate axis. These vectors have a sparseness measure of 1.
   The &quot;least sparse&quot; vector is one for which elements are equal. These vectors have a sparseness of 0.
*/</span>
<span style="color: #006400; font-style: italic;">/* first, visualize for v on the unit circle in 2-D */</span>
<span style="color: #000080; font-weight: bold;">data</span> circle_Hoyer;
   sn = <span style="color: #0000ff;">sqrt</span><span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#41;</span>;
   pi = constant<span style="color: #66cc66;">&#40;</span><span style="color: #a020f0;">'pi'</span><span style="color: #66cc66;">&#41;</span>;
   <span style="color: #0000ff;">do</span> theta = <span style="color: #2e8b57; font-weight: bold;">0</span> to <span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #006400; font-style: italic;">*pi by pi/100;</span>
      <span style="color: #0000ff;">x</span> = <span style="color: #0000ff;">cos</span><span style="color: #66cc66;">&#40;</span>theta<span style="color: #66cc66;">&#41;</span>;
      y = <span style="color: #0000ff;">sin</span><span style="color: #66cc66;">&#40;</span>theta<span style="color: #66cc66;">&#41;</span>;
      L1 = lpnorm<span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">1</span>, <span style="color: #0000ff;">x</span>, y<span style="color: #66cc66;">&#41;</span>;  <span style="color: #006400; font-style: italic;">/* L1 norm of v=(x,y) */</span>
      Sparseness = <span style="color: #66cc66;">&#40;</span>sn - L1<span style="color: #66cc66;">&#41;</span> / <span style="color: #66cc66;">&#40;</span>sn-<span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#41;</span>;
      <span style="color: #0000ff;">output</span>;
   <span style="color: #0000ff;">end</span>;
<span style="color: #000080; font-weight: bold;">run</span>;
&nbsp;
<span style="color: #006400; font-style: italic;">/* use Spectral color ramp from ColorBrewer */</span>
<span style="color: #0000ff;">%let</span> spectral = CX3288BD CX99D594 CXE6F598 CXFFFFBF CXFEE08B CXFC8D59 CXD53E4F;
<span style="color: #006400; font-style: italic;">/* Visualize Hoyer's sparseness measure on the unit circle */</span>
<span style="color: #0000ff;">title</span> <span style="color: #a020f0;">&quot;Sparseness on the Unit Circle&quot;</span>;
<span style="color: #000080; font-weight: bold;">proc sgplot</span> <span style="color: #000080; font-weight: bold;">data</span>=circle_Hoyer aspect=<span style="color: #2e8b57; font-weight: bold;">1</span>;
   scatter <span style="color: #0000ff;">x</span>=<span style="color: #0000ff;">x</span> y=y / colorresponse=Sparseness colormodel=<span style="color: #66cc66;">&#40;</span><span style="color: #0000ff; font-weight: bold;">&amp;spectral</span><span style="color: #66cc66;">&#41;</span> markerattrs=<span style="color: #66cc66;">&#40;</span>symbol=CircleFilled<span style="color: #66cc66;">&#41;</span>;
   gradlegend / <span style="color: #0000ff;">title</span>=<span style="color: #a020f0;">&quot;L1 Norm&quot;</span>;
   xaxis grid; yaxis grid;
<span style="color: #000080; font-weight: bold;">run</span>;</pre></td></tr></table></div>





<a href="https://blogs.sas.com/content/iml/files/2026/07/sparseness1.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/07/sparseness1.png" alt="" width="480" height="360" class="alignnone size-full wp-image-59550" srcset="https://blogs.sas.com/content/iml/files/2026/07/sparseness1.png 640w, https://blogs.sas.com/content/iml/files/2026/07/sparseness1-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a>

<p>
As expected, the visualization shows that the Hoyer sparseness measure is 0 (blue) for the vectors (&plusmn;1/&radic;2, &plusmn;1/&radic;2), which have components that are equal in magnitude.
The measure is 1 (red) for vectors on a coordinate axis.
Notice that I used <a href="https://go.documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/lefunctionsref/n0ppk0d4ci0ai7n1c1tlkqc1j4ff.htm">the LPNORM function in Base SAS</a> to compute the L1 norm of the vectors.
</p>


<h3>Visualize Hoyer's sparseness for 3-D vectors</h3>
<p>
In a similar way, you can visualize the sparseness of 3-D unit vectors. 
The measure will obtain its maximum value of 1 for vectors that are aligned with a coordinate axis.
It will obtain its minimum value of 0 when the vectors components are equal in magnitude: (&plusmn;1/&radic;3, &plusmn;1/&radic;3, &plusmn;1/&radic;3).
</p>
<p>
The following SAS DATA step visualizes the Hoyer sparseness measure for 3-D vectors on the upper hemisphere of the unit sphere:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* Generate the grid for the upper hemisphere */</span>
<span style="color: #000080; font-weight: bold;">data</span> sphere_L1;
   sn = <span style="color: #0000ff;">sqrt</span><span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">3</span><span style="color: #66cc66;">&#41;</span>;
   delta = <span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #006400; font-style: italic;">**(-5);</span>     <span style="color: #006400; font-style: italic;">/* Set the resolution of the grid */</span>
   <span style="color: #006400; font-style: italic;">/* Loop over (x,y) in unit circle, subset of [-1,1]x[-1,1] */</span>
   <span style="color: #0000ff;">do</span> <span style="color: #0000ff;">x</span> = -<span style="color: #2e8b57; font-weight: bold;">1</span> to <span style="color: #2e8b57; font-weight: bold;">1</span> <span style="color: #0000ff;">by</span> delta;
      <span style="color: #0000ff;">do</span> y = -<span style="color: #2e8b57; font-weight: bold;">1</span> to <span style="color: #2e8b57; font-weight: bold;">1</span> <span style="color: #0000ff;">by</span> delta;
         <span style="color: #0000ff;">if</span> <span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">x</span><span style="color: #006400; font-style: italic;">**2 + y**2) &lt;= 1 then do;</span> <span style="color: #006400; font-style: italic;">/* restrict to unit circle */</span>
            z = <span style="color: #0000ff;">sqrt</span><span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">1</span> - <span style="color: #0000ff;">x</span><span style="color: #006400; font-style: italic;">**2 - y**2);</span>  <span style="color: #006400; font-style: italic;">/* upper hemisphere */</span>
            L1 = lpnorm<span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">1</span>, <span style="color: #0000ff;">x</span>, y, z<span style="color: #66cc66;">&#41;</span>;    <span style="color: #006400; font-style: italic;">/* L1 norm of v=(x,y,z) */</span>
            Sparseness = <span style="color: #66cc66;">&#40;</span>sn - L1<span style="color: #66cc66;">&#41;</span> / <span style="color: #66cc66;">&#40;</span>sn-<span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#41;</span>;
            <span style="color: #0000ff;">output</span>;
         <span style="color: #0000ff;">end</span>;
      <span style="color: #0000ff;">end</span>;
   <span style="color: #0000ff;">end</span>;
   <span style="color: #0000ff;">drop</span> delta;
<span style="color: #000080; font-weight: bold;">run</span>;
&nbsp;
<span style="color: #006400; font-style: italic;">/* use Spectral color ramp from ColorBrewer */</span>
<span style="color: #0000ff;">%let</span> spectral = CX3288BD CX99D594 CXE6F598 CXFFFFBF CXFEE08B CXFC8D59 CXD53E4F;
<span style="color: #006400; font-style: italic;">/* Visualize the L1 norm as a heatmap */</span>
<span style="color: #0000ff;">title</span> <span style="color: #a020f0;">&quot;Heatmap of Sparseness on the Upper Hemisphere of the Unit Sphere&quot;</span>;
<span style="color: #000080; font-weight: bold;">proc sgplot</span> <span style="color: #000080; font-weight: bold;">data</span>=sphere_L1 aspect=<span style="color: #2e8b57; font-weight: bold;">1</span>;
   heatmapparm <span style="color: #0000ff;">x</span>=<span style="color: #0000ff;">x</span> y=y colorresponse=Sparseness / colormodel=<span style="color: #66cc66;">&#40;</span><span style="color: #0000ff; font-weight: bold;">&amp;spectral</span><span style="color: #66cc66;">&#41;</span>;        
   gradlegend / <span style="color: #0000ff;">title</span>=<span style="color: #a020f0;">&quot;L1 Norm&quot;</span>;
<span style="color: #000080; font-weight: bold;">run</span>;</pre></td></tr></table></div>




<a href="https://blogs.sas.com/content/iml/files/2026/07/sparseness2.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/07/sparseness2.png" alt="" width="480" height="360" class="alignnone size-full wp-image-59547" srcset="https://blogs.sas.com/content/iml/files/2026/07/sparseness2.png 640w, https://blogs.sas.com/content/iml/files/2026/07/sparseness2-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a>

<p>
Because of the symmetries in the Hoyer sparseness measure, it suffices to visualize the measure on vectors in first octant where all elements are nonnegative. In the following image, I used a DROPLINE statement to locate the vector that has minimal Hoyer sparseness, which is (1/sqrt(3), 1/sqrt(3), 1/sqrt(3)).
</p>

<a href="https://blogs.sas.com/content/iml/files/2026/07/sparseness3.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/07/sparseness3.png" alt="" width="480" height="360" class="alignnone size-full wp-image-59544" srcset="https://blogs.sas.com/content/iml/files/2026/07/sparseness3.png 640w, https://blogs.sas.com/content/iml/files/2026/07/sparseness3-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a>



<h3>A SAS function to compute Hoyer's sparseness</h3>
<p>
In SAS, the IML language is the natural place to implement a vector measure because the language supports vector operations in a natural way.
The following PROC IML statements define a function that computes Hoyer's sparseness for a vector:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #000080; font-weight: bold;">proc iml</span>;
<span style="color: #006400; font-style: italic;">/* Hoyer's sparseness measure.
   For a column vector, v, this function returns 
   the Hoyer (2004) measure of sparseness
   s = (sqrt(n) - ||v||_1 / ||v||_2) / (sqrt(n)-1)
*/</span>
start Sparseness<span style="color: #66cc66;">&#40;</span>v<span style="color: #66cc66;">&#41;</span>;
   u = colvec<span style="color: #66cc66;">&#40;</span>v<span style="color: #66cc66;">&#41;</span>;    <span style="color: #006400; font-style: italic;">/* always treat input as a column vector */</span>
   L2 = norm<span style="color: #66cc66;">&#40;</span>u, <span style="color: #a020f0;">&quot;L2&quot;</span><span style="color: #66cc66;">&#41;</span>;
   <span style="color: #0000ff;">if</span> L2=<span style="color: #2e8b57; font-weight: bold;">0</span> <span style="color: #0000ff;">then</span> <span style="color: #0000ff;">return</span><span style="color: #66cc66;">&#40;</span>.<span style="color: #66cc66;">&#41;</span>;
   L1 = norm<span style="color: #66cc66;">&#40;</span>u, <span style="color: #a020f0;">&quot;L1&quot;</span><span style="color: #66cc66;">&#41;</span>;
   sn = <span style="color: #0000ff;">sqrt</span><span style="color: #66cc66;">&#40;</span> countn<span style="color: #66cc66;">&#40;</span>u<span style="color: #66cc66;">&#41;</span> <span style="color: #66cc66;">&#41;</span>;
   sparseness = <span style="color: #66cc66;">&#40;</span>sn - L1/L2<span style="color: #66cc66;">&#41;</span> / <span style="color: #66cc66;">&#40;</span>sn - <span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#41;</span>;
   <span style="color: #0000ff;">return</span><span style="color: #66cc66;">&#40;</span> clip01<span style="color: #66cc66;">&#40;</span>sparseness<span style="color: #66cc66;">&#41;</span> <span style="color: #66cc66;">&#41;</span>;
finish;
<span style="color: #006400; font-style: italic;">/* Helper function: clip values into [0,1]
   If x is any matrix, return a new matrix whose i_th element has the value 
   0    if x[i] &lt; 0
   x[i] if 0 &lt;= x[i] &lt;= 1
   1    if x[i] &gt; 1
   See https://blogs.sas.com/content/iml/2026/02/04/clip-values.html */</span>
start clip01<span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">x</span><span style="color: #66cc66;">&#41;</span>;
   <span style="color: #0000ff;">return</span><span style="color: #66cc66;">&#40;</span> <span style="color: #2e8b57; font-weight: bold;">0</span> &lt;&gt; <span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">x</span> &gt;&lt; <span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#41;</span> <span style="color: #66cc66;">&#41;</span>;
finish;
&nbsp;
<span style="color: #006400; font-style: italic;">/* Examples: Compute the Hoyer measure for these columns */</span>
<span style="color: #006400; font-style: italic;">/*   q       r  s       t    u       v */</span>  
A = <span style="color: #66cc66;">&#123;</span><span style="color: #2e8b57; font-weight: bold;">0.4082</span>  <span style="color: #2e8b57; font-weight: bold;">1</span>  <span style="color: #2e8b57; font-weight: bold;">0.5774</span>  <span style="color: #2e8b57; font-weight: bold;">3.3</span>  <span style="color: #2e8b57; font-weight: bold;">0.9948</span>  <span style="color: #2e8b57; font-weight: bold;">1</span>,
     <span style="color: #2e8b57; font-weight: bold;">0.4082</span>  <span style="color: #2e8b57; font-weight: bold;">1</span>  <span style="color: #2e8b57; font-weight: bold;">0.5774</span>  <span style="color: #2e8b57; font-weight: bold;">1</span>    <span style="color: #2e8b57; font-weight: bold;">0.08</span>    <span style="color: #2e8b57; font-weight: bold;">0</span>,
     <span style="color: #2e8b57; font-weight: bold;">0.4082</span>  <span style="color: #2e8b57; font-weight: bold;">1</span>  <span style="color: #2e8b57; font-weight: bold;">0.5774</span>  <span style="color: #2e8b57; font-weight: bold;">1</span>    <span style="color: #2e8b57; font-weight: bold;">0.04</span>    <span style="color: #2e8b57; font-weight: bold;">0</span>,
     <span style="color: #2e8b57; font-weight: bold;">0.4082</span>  <span style="color: #2e8b57; font-weight: bold;">1</span>  <span style="color: #2e8b57; font-weight: bold;">0</span>       <span style="color: #2e8b57; font-weight: bold;">0.3</span>  <span style="color: #2e8b57; font-weight: bold;">0.04</span>    <span style="color: #2e8b57; font-weight: bold;">0</span>,
     <span style="color: #2e8b57; font-weight: bold;">0.4082</span>  <span style="color: #2e8b57; font-weight: bold;">1</span>  <span style="color: #2e8b57; font-weight: bold;">0</span>       <span style="color: #2e8b57; font-weight: bold;">0.2</span>  <span style="color: #2e8b57; font-weight: bold;">0.02</span>    <span style="color: #2e8b57; font-weight: bold;">0</span>,
     <span style="color: #2e8b57; font-weight: bold;">0.4082</span>  <span style="color: #2e8b57; font-weight: bold;">1</span>  <span style="color: #2e8b57; font-weight: bold;">0</span>       <span style="color: #2e8b57; font-weight: bold;">0.2</span>  <span style="color: #2e8b57; font-weight: bold;">0.02</span>    <span style="color: #2e8b57; font-weight: bold;">0</span><span style="color: #66cc66;">&#125;</span>;
&nbsp;
S = j<span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">1</span>, ncol<span style="color: #66cc66;">&#40;</span>A<span style="color: #66cc66;">&#41;</span>, .<span style="color: #66cc66;">&#41;</span>;
<span style="color: #0000ff;">do</span> i = <span style="color: #2e8b57; font-weight: bold;">1</span> to ncol<span style="color: #66cc66;">&#40;</span>A<span style="color: #66cc66;">&#41;</span>;
   S<span style="color: #66cc66;">&#91;</span>i<span style="color: #66cc66;">&#93;</span> = Sparseness<span style="color: #66cc66;">&#40;</span> A<span style="color: #66cc66;">&#91;</span>,i<span style="color: #66cc66;">&#93;</span> <span style="color: #66cc66;">&#41;</span>;
<span style="color: #0000ff;">end</span>;
print S<span style="color: #66cc66;">&#91;</span>F=<span style="color: #2e8b57; font-weight: bold;">6.4</span> c=<span style="color: #66cc66;">&#40;</span><span style="color: #a020f0;">'q'</span>:<span style="color: #a020f0;">'w'</span><span style="color: #66cc66;">&#41;</span> L=<span style="color: #a020f0;">&quot;Hoyer's Sparseness&quot;</span><span style="color: #66cc66;">&#93;</span>;</pre></td></tr></table></div>




<img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/07/sparseness4.png" alt="" width="313" height="98" class="alignnone size-full wp-image-59586" srcset="https://blogs.sas.com/content/iml/files/2026/07/sparseness4.png 313w, https://blogs.sas.com/content/iml/files/2026/07/sparseness4-300x94.png 300w" sizes="(max-width: 313px) 100vw, 313px" />

<p>
The table shows the sparseness for each column of the A matrix. 
</p>
<ul>
<li>
The first and second columns (q and r) both have sparseness 0 because both columns are constant in value. The first column has unit L2 norm whereas the second column does not. But it doesn't matter, because the Hoyer measure is normalized by using the ratio 
||<strong>v</strong>||<sub>1</sub> / ||<strong>v</strong>||<sub>2</sub>.
</li>
<li>
The third column (s) has three zero elements and three equal and nonzero elements.  The sparseness measure is 0.495.
</li>
<li>
The fourth column (t) has one large element, two medium-sized elements, and three small but nonzero elements.
The sparseness measure is 0.5445.
</li>
<li>
The fifth column (u) has one large element and five small but nonzero elements.
The sparseness measure is 0.8656.
</li>
<li>
The last column (v) has one large element and five elements that are zero.
The sparseness measure is 1, which is the largest possible value of sparseness.
</li>
</ul>

<p>
An earlier section visualized three of these columns in a panel of needle plots.
</p>

<h3>Summary</h3>
<p>
This article introduces the Hoyer sparseness measure, S(<strong>v</strong>), for nonzero vectors <strong>v</strong>.
The measure indicates whether a vector has many components that are the same magnitude (low sparseness) as opposed to vectors that have many small components and only a few components that are large in magnitude. The sparseness measure is invariant to scaling, permutation of components, and switching the signs of components. Accordingly, you can visualize the measure by using vectors whose components are all positive and whose Euclidean length is 1.
</p>
<p>
A future article shows an application of the Hoyer measure to the nonnegative matrix factorization (NMF) method.
</p>



<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/07/27/hoyer-sparseness.html">Hoyer&#039;s sparseness measure</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blogs.sas.com/content/iml/2026/07/27/hoyer-sparseness.html/feed</wfw:commentRss>
			<slash:comments>5</slash:comments>
		
		
			<enclosure url="https://blogs.sas.com/content/iml/files/2026/07/sparseness2-150x150.png" />
	</item>
		<item>
		<title>The log-KDE method: Density estimates for positive data</title>
		<link>https://blogs.sas.com/content/iml/2026/07/20/log-kde-positive-data.html</link>
					<comments>https://blogs.sas.com/content/iml/2026/07/20/log-kde-positive-data.html#comments</comments>
		
		<dc:creator><![CDATA[Rick Wicklin]]></dc:creator>
		<pubDate>Mon, 20 Jul 2026 09:20:46 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Data Analysis]]></category>
		<category><![CDATA[Statistical Programming]]></category>
		<guid isPermaLink="false">https://blogs.sas.com/content/iml/?p=59447</guid>

					<description><![CDATA[<p>A previous article discusses the problem of fitting a kernel density estimate (KDE) to data that are strictly positive. If you use a standard KDE, the resulting density curve might estimate non-zero probability for negative values. This is unsatisfactory because quantities like lengths and mass cannot be negative. The previous [...]</p>
<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/07/20/log-kde-positive-data.html">The log-KDE method: Density estimates for positive data</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>
A previous article discusses <a href="https://blogs.sas.com/content/iml/2026/07/13/kde-positive.html">the problem of fitting a kernel density estimate (KDE) to data that are strictly positive</a>. 
If you use a standard KDE, the resulting density curve might estimate non-zero probability for negative values. 
This is unsatisfactory because quantities like lengths and mass cannot be negative.
The previous article shows how to use PROC KDE in SAS to truncate the KDE (and optionally renormalize the density).
</p>
<p>
Unfortunately, truncation has a known statistical problem: the density is biased near the boundary x=0 (Silverman, 1986).
This article demonstrates an alternative: the log-KDE method. 
The log-KDE method guarantees that the estimated density is strictly positive and integrates to unity.
Furthermore, the resulting density estimate is not biased near the boundary.
</p>

<h3>The Log-KDE method</h3>
<p>
Let X be a strictly positive random variable. We want to compute a nonparametric density estimate of the distribution of X.
The log-KDE method consists of three steps:
</p>
<ol>
<li>Log-transform the strictly positive data: <em>Y</em> = log(<em>X</em>).</li>
<li>Fit a standard kernel density estimate to the transformed data, <em>Y</em>.</li>
<li>Back-transform the density to the original scale.</li>
</ol>

<p>
Step 3 is a little tricky.
Let <em>f<sub>Y</sub></em>(<em>y</em>) be the KDE on the log scale.
When you back-transform the density to the data scale, 
you must apply a change-of-variables formula, also known as the chain rule or the Jacobian of the transformation.
This is familiar to students of integral calculus: when you change variables inside an integral, you also must 
change the differential.
</p><p>
For a general change-of-variables formula, consider the integral
<br />
&int;  <em>f<sub>Y</sub>(y) dy</em>
<br />
If you represent this integral in terms of x by making the substitution <em>y</em> = log(<em>x</em>),
then <em>dy</em> =  <em>dy/dx</em> <em>dx</em> = (1/<em>x</em>)  <em>dx</em>.
Thought of as measures, 
the back-transformed density is therefore
<br />
<em>f<sub>X</sub></em>(<em>x</em>) = <em>f<sub>Y</sub></em>(log(<em>x</em>)) (1/<em>x</em>)
</p>

<h3>Implement the method in SAS</h3>
<p>
Let's implement the log-KDE method for the same `Bulkhead` dataset used in the previous article. 
The data represent the position of a rivet relative to a bulkhead, which must be a positive quantity.
The following DATA step defines the data.
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #000080; font-weight: bold;">data</span> Bulkhead;
<span style="color: #0000ff;">input</span> Position @@;
datalines;
 <span style="color: #2e8b57; font-weight: bold;">2.44</span>  <span style="color: #2e8b57; font-weight: bold;">6.84</span> <span style="color: #2e8b57; font-weight: bold;">16.84</span>  <span style="color: #2e8b57; font-weight: bold;">3.76</span>  <span style="color: #2e8b57; font-weight: bold;">5.58</span> <span style="color: #2e8b57; font-weight: bold;">14.53</span> <span style="color: #2e8b57; font-weight: bold;">12.12</span>  <span style="color: #2e8b57; font-weight: bold;">8.00</span>  <span style="color: #2e8b57; font-weight: bold;">8.66</span>  <span style="color: #2e8b57; font-weight: bold;">6.63</span>  <span style="color: #2e8b57; font-weight: bold;">9.51</span>  <span style="color: #2e8b57; font-weight: bold;">6.37</span> <span style="color: #2e8b57; font-weight: bold;">11.78</span>
 <span style="color: #2e8b57; font-weight: bold;">6.14</span>  <span style="color: #2e8b57; font-weight: bold;">9.04</span>  <span style="color: #2e8b57; font-weight: bold;">2.88</span> <span style="color: #2e8b57; font-weight: bold;">21.28</span>  <span style="color: #2e8b57; font-weight: bold;">8.43</span> <span style="color: #2e8b57; font-weight: bold;">18.91</span>  <span style="color: #2e8b57; font-weight: bold;">4.45</span>  <span style="color: #2e8b57; font-weight: bold;">4.59</span> <span style="color: #2e8b57; font-weight: bold;">20.42</span>  <span style="color: #2e8b57; font-weight: bold;">9.03</span>  <span style="color: #2e8b57; font-weight: bold;">6.55</span>  <span style="color: #2e8b57; font-weight: bold;">7.15</span>  <span style="color: #2e8b57; font-weight: bold;">4.10</span>
 <span style="color: #2e8b57; font-weight: bold;">8.72</span>  <span style="color: #2e8b57; font-weight: bold;">5.17</span>  <span style="color: #2e8b57; font-weight: bold;">2.41</span> <span style="color: #2e8b57; font-weight: bold;">10.93</span> <span style="color: #2e8b57; font-weight: bold;">14.08</span>  <span style="color: #2e8b57; font-weight: bold;">5.44</span>  <span style="color: #2e8b57; font-weight: bold;">2.81</span>  <span style="color: #2e8b57; font-weight: bold;">3.99</span>  <span style="color: #2e8b57; font-weight: bold;">8.40</span> <span style="color: #2e8b57; font-weight: bold;">11.28</span>  <span style="color: #2e8b57; font-weight: bold;">3.97</span> <span style="color: #2e8b57; font-weight: bold;">18.99</span> <span style="color: #2e8b57; font-weight: bold;">10.51</span>
 <span style="color: #2e8b57; font-weight: bold;">9.52</span>  <span style="color: #2e8b57; font-weight: bold;">2.01</span>  <span style="color: #2e8b57; font-weight: bold;">1.5</span> 
;
<span style="color: #0000ff;">%let</span> <span style="color: #0000ff;">DSName</span>  = Bulkhead;
<span style="color: #0000ff;">%let</span> <span style="color: #0000ff;">varName</span> = Position;</pre></td></tr></table></div>




<p>
First, use the DATA step to log-transform the data,
Then, use PROC KDE to estimate the density on the unbounded log scale. 
You can use the OUT= option to save the evaluation points and density values to a dataset, as follows:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* 1. Log-transform the data */</span>
<span style="color: #000080; font-weight: bold;">data</span> LogData;
  <span style="color: #0000ff;">set</span> &amp;<span style="color: #0000ff;">DSName</span>;
  Log_&amp;<span style="color: #0000ff;">varName</span> = <span style="color: #0000ff;">log</span><span style="color: #66cc66;">&#40;</span>&amp;<span style="color: #0000ff;">varName</span><span style="color: #66cc66;">&#41;</span>;
<span style="color: #000080; font-weight: bold;">run</span>;
&nbsp;
<span style="color: #006400; font-style: italic;">/* 2. Fit a density estimate to the transformed data */</span>
<span style="color: #000080; font-weight: bold;">proc kde</span> <span style="color: #000080; font-weight: bold;">data</span>=LogData;
   univar Log_&amp;<span style="color: #0000ff;">varName</span> / unistats 
          out=KDEOUT_Log<span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">rename</span>=<span style="color: #66cc66;">&#40;</span>value=log_value density=KDE_LogDensity<span style="color: #66cc66;">&#41;</span><span style="color: #66cc66;">&#41;</span>;
<span style="color: #000080; font-weight: bold;">run</span>;</pre></td></tr></table></div>




<p>
The 'KDEOUT_Log' data set contains the density (KDE_LogDensity) of the log-transformed variable (log_value).
Next, use the DATA step to apply the change-of-variables formula, which maps the log-density back to the data scale:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* 3. Back-transform the density using f_X(x) = f_Y(log(x)) * (1/x), where 1/x is the Jacobian */</span>
<span style="color: #000080; font-weight: bold;">data</span> KDEOUT;
   <span style="color: #0000ff;">set</span> KDEOUT_Log;
   <span style="color: #0000ff;">x</span> = <span style="color: #0000ff;">exp</span><span style="color: #66cc66;">&#40;</span>log_value<span style="color: #66cc66;">&#41;</span>;
   KDE_Density = KDE_LogDensity / <span style="color: #0000ff;">x</span>;  
   <span style="color: #0000ff;">drop</span> <span style="color: #0000ff;">var</span> log_value KDE_LogDensity;
<span style="color: #000080; font-weight: bold;">run</span>;</pre></td></tr></table></div>




<h3>Visualize the back-transformed KDE</h3>
<p>
You can overlay the newly calculated density curve (in the KDE_Density variable), onto a histogram of the original data. 
As shown in a previous article, you can <a href="https://blogs.sas.com/content/iml/2025/10/20/overlay-curves-histogram.html">overlay custom density curves on a histogram</a> by using the <a href="https://blogs.sas.com/content/iml/2025/10/13/high-low-emulate-histogram.html">%EmulateHistogram macro</a>.
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* Create macro variables for histogram bins and observation count.
   Download the macro from https://blogs.sas.com/content/iml/2025/10/13/high-low-emulate-histogram.html */</span>
%EmulateHistogram<span style="color: #66cc66;">&#40;</span>dsIn=&amp;<span style="color: #0000ff;">DSName</span>, varIn=&amp;<span style="color: #0000ff;">varName</span><span style="color: #66cc66;">&#41;</span>;
&nbsp;
<span style="color: #006400; font-style: italic;">/* Scale the density to the percentage (or count) scale of the histogram */</span>
<span style="color: #000080; font-weight: bold;">data</span> All;
   <span style="color: #0000ff;">set</span> &amp;<span style="color: #0000ff;">DSName</span> _HistBins KDEOut;
   KDE_Percent = <span style="color: #2e8b57; font-weight: bold;">100</span>    <span style="color: #006400; font-style: italic;">* &amp;_binWidth * KDE_Density;</span>
   KDE_Count   = <span style="color: #0000ff; font-weight: bold;">&amp;_NOBS</span> <span style="color: #006400; font-style: italic;">* &amp;_binWidth * KDE_Density;</span>
<span style="color: #000080; font-weight: bold;">run</span>;
&nbsp;
<span style="color: #006400; font-style: italic;">/* 4) Overlay on a histogram */</span>
<span style="color: #0000ff;">title</span> <span style="color: #a020f0;">&quot;Back-Transform of Fit of log(Position)&quot;</span>;
<span style="color: #000080; font-weight: bold;">proc sgplot</span> <span style="color: #000080; font-weight: bold;">data</span>=All noautolegend;
   highlow <span style="color: #0000ff;">x</span>=_midpt_ low=_zero_ high=_pct_ / type=bar barwidth=<span style="color: #2e8b57; font-weight: bold;">1</span>;
   series <span style="color: #0000ff;">x</span>=<span style="color: #0000ff;">x</span> y=KDE_Percent;
   fringe &amp;<span style="color: #0000ff;">varName</span>;
   yaxis <span style="color: #0000ff;">min</span>=<span style="color: #2e8b57; font-weight: bold;">0</span> offsetmin=<span style="color: #2e8b57; font-weight: bold;">0</span> grid;
   xaxis values=<span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">0</span> to <span style="color: #0000ff; font-weight: bold;">&amp;_binEnd</span> <span style="color: #0000ff;">by</span> <span style="color: #0000ff; font-weight: bold;">&amp;_binWidth</span><span style="color: #66cc66;">&#41;</span>;
<span style="color: #000080; font-weight: bold;">run</span>;</pre></td></tr></table></div>




<figure id="attachment_59456" aria-describedby="caption-attachment-59456" style="width: 480px" class="wp-caption alignnone"><a href="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc6.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc6.png" alt="" width="480" height="360" class="size-full wp-image-59456" srcset="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc6.png 640w, https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc6-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a><figcaption id="caption-attachment-59456" class="wp-caption-text">Log-KDE for Positive Data</figcaption></figure>

<p>
Notice that the KDE is zero when the Position variable is negative. Furthermore, this KDE integrates to unity.
The estimate of the KDE near zero is data dependent.
For these data, the KDE drops smoothly to zero at Positon=0. 
Contrast that with the estimate in the previous article, which predicted a nonzero probability when Position &asymp; 0.
</p>

<h3>Peculiarities of the log-KDE method</h3>
<p>
It is important to understand that this log-KDE estimate is different from the standard KDE on the data scale.
Most KDEs use a constant bandwidth for the kernels. 
Intuitively, <a href="https://blogs.sas.com/content/iml/2016/07/27/visualize-kernel-density-estimate.html">you can visualize the KDE as the sum of a bunch of little bump functions</a> (the kernels) centered at each data point. The most common bump functions a Gaussian in shape.
But that is not true for the log-KDE method.
</p><p>
For the log-KDE method, Gaussian kernels are used to fit the density on the log scale.
But when you back-transform the density, the Gaussian kernels are transformed into lognormal kernels. (Recall that the definition of a lognormal variable is one whose logarithm is normal.)
Furthermore, the bandwidth of the lognormal kernels is not uniform. 
</p><p>
To see this, consider a Gaussian kernel centered on the point <em>y</em><sub>i</sub> = log(<em>x</em><sub>i</sub>) in the log-scale coordinate system.
It has a bandwidth, <em>h</em>, which determines the scale of the kernel.
The back-transformation maps the Gaussian kernel to a lognormal kernel that has
location parameter log(<em>x</em><sub>i</sub>) and scale parameter <em>h</em>.
<a href="https://en.wikipedia.org/wiki/Log-normal_distribution">The standard deviation of that lognormal distribution</a> is therefore
<br />
exp(log(<em>x</em><sub>i</sub>) + h<sup>2</sup>/2) sqrt(exp(h<sup>2</sup>) - 1)
<br />
or 
<br />
<em>x</em><sub>i</sub> exp(h<sup>2</sup>/2) sqrt(exp(h<sup>2</sup>) - 1)
<br />
</p><p>
The interpretation of this formula is that the width of the (lognormal) kernels on the data scale are 
proportional to the data value, 
<em>x</em><sub>i</sub>.
For data values much less than 1, the kernels on the data scale are tall and narrow
whereas for values much greater than 1, the kernels on the data scale are short and broad.
</p><p>
For example, suppose you have the data values {0.2, 0.5, 1, 2}.
These get log-transformed and the KDE is computed on the log scale by placing a Gaussian density at 
log(<em>x</em><sub>i</sub>) for each <em>i</em>.
Now consider what these bell-shaped curves will look like when they are back-transformed.
Each becomes a lognormal curve, but the width of those curves is not constant.
This is illustrated in the following graph, which shows 
the kernels on the data scale for the data values {0.2, 0.5, 1, 2}.
</p>


<a href="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc7.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc7.png" alt="" width="480" height="360" class="alignnone size-full wp-image-59468" srcset="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc7.png 640w, https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc7-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a>

<p>
As you can see, the kernel based at <em>x</em>=0.2 is tall and narrow, whereas the kernel based at <em>x</em>=2 is broad and flat.
</p>
<p>
Because of this, you might notice spikes in the log-KDE for 
data that are near 0. For example, if you were to add a new  data point to the Bulkhead data set for Position=0.5, 
the log-KDE will become bimodal and exhibit a new wiggle near that location.
</p>

<h3>Summary</h3>
<p>
This article shows how to use the log-KDE method to estimate the density of strictly positive data.
The method is guaranteed to estimate a density who support is strictly positive.
Furthermore, the resulting density estimate is not biased near the boundary.
However, geometrically, this is achieved by using lognormal kernels with nonconstant 
bandwidths. The standard deviation of a lognormal kernel is proportional to the data value at which the kernel is based.
</p>

<h3>Further Reading</h3>
<p>
Jones, A. T., Nguyen, H. D., &amp; McLachlan, G. J. (2018). <a href="https://cran.r-project.org/web/packages/logKDE/vignettes/logKDE.pdf">"Kernel density estimation on positive data via the logKDE package for R."</a> The
<a href="https://cran.r-project.org/web/packages/logKDE/index.html">logKDE package</a> is available on CRAN.

</p><p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/07/20/log-kde-positive-data.html">The log-KDE method: Density estimates for positive data</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blogs.sas.com/content/iml/2026/07/20/log-kde-positive-data.html/feed</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			<enclosure url="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc6-150x150.png" />
	</item>
		<item>
		<title>Kernel density estimates for positive data</title>
		<link>https://blogs.sas.com/content/iml/2026/07/13/kde-positive.html</link>
					<comments>https://blogs.sas.com/content/iml/2026/07/13/kde-positive.html#comments</comments>
		
		<dc:creator><![CDATA[Rick Wicklin]]></dc:creator>
		<pubDate>Mon, 13 Jul 2026 09:28:26 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Data Analysis]]></category>
		<category><![CDATA[SAS Programming]]></category>
		<category><![CDATA[Statistical Programming]]></category>
		<guid isPermaLink="false">https://blogs.sas.com/content/iml/?p=56896</guid>

					<description><![CDATA[<p>The kernel density estimate (KDE) is a powerful tool for estimating the density of univariate data. The KDE is a flexible model. For example, it can fit data distributions that are multimodal or have long tails. It is nonparametric, which means that you do not need to assume any form [...]</p>
<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/07/13/kde-positive.html">Kernel density estimates for positive data</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>
The kernel density estimate (KDE) is a powerful tool for estimating the density of univariate data.
The KDE is a flexible model. For example, it can fit data distributions that are multimodal or have long tails.
It is nonparametric, which means that 
you do not need to assume any form for the distribution of the data.
In contrast, parametric estimates (such as normal or lognormal distributions) impose 
constraints on the shape of the data distribution.
</p><p>
However, the KDE's flexibility comes at a cost. If the underlying distribution is known to have certain properties, 
the KDE does not necessarily preserve those properties.
A property that arises often in practice is <em>positivity</em>. 
Many real-world measurements are strictly positive, 
such as mass, length, and many health-related clinical lab values.
If you have a distribution of lengths, and you fit a KDE to the data, it is disconcerting (and wrong!) to 
display a density estimate that predicts positive density for negative lengths!
</p>
<p>
There are two ways to deal with this problem. 
This article shows how to use SAS to truncate the density estimate 
by setting the density to zero when the variable is negative. 
A subsequent article shows how to log-transform the data, fit a KDE on the log scale, and back-transform the results in SAS. 
</p>

<h3>Fit a KDE to positive data</h3>
<p>
The following data set shows the position (in millimeters) of a rivet 
relative to a bulkhead. The distance is a positive quantity.
For convenience, I assign the name of the data sets and the variables to macro variables.
The remainder of the examples use only the macro variables, which makes it easy for you to
run the code on your own data.
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #000080; font-weight: bold;">data</span> Bulkhead;
<span style="color: #0000ff;">input</span> Position @@;
datalines;
 <span style="color: #2e8b57; font-weight: bold;">2.44</span>  <span style="color: #2e8b57; font-weight: bold;">6.84</span> <span style="color: #2e8b57; font-weight: bold;">16.84</span>  <span style="color: #2e8b57; font-weight: bold;">3.76</span>  <span style="color: #2e8b57; font-weight: bold;">5.58</span> <span style="color: #2e8b57; font-weight: bold;">14.53</span> <span style="color: #2e8b57; font-weight: bold;">12.12</span>  <span style="color: #2e8b57; font-weight: bold;">8.00</span>  <span style="color: #2e8b57; font-weight: bold;">8.66</span>  <span style="color: #2e8b57; font-weight: bold;">6.63</span>  <span style="color: #2e8b57; font-weight: bold;">9.51</span>  <span style="color: #2e8b57; font-weight: bold;">6.37</span> <span style="color: #2e8b57; font-weight: bold;">11.78</span>
 <span style="color: #2e8b57; font-weight: bold;">6.14</span>  <span style="color: #2e8b57; font-weight: bold;">9.04</span>  <span style="color: #2e8b57; font-weight: bold;">2.88</span> <span style="color: #2e8b57; font-weight: bold;">21.28</span>  <span style="color: #2e8b57; font-weight: bold;">8.43</span> <span style="color: #2e8b57; font-weight: bold;">18.91</span>  <span style="color: #2e8b57; font-weight: bold;">4.45</span>  <span style="color: #2e8b57; font-weight: bold;">4.59</span> <span style="color: #2e8b57; font-weight: bold;">20.42</span>  <span style="color: #2e8b57; font-weight: bold;">9.03</span>  <span style="color: #2e8b57; font-weight: bold;">6.55</span>  <span style="color: #2e8b57; font-weight: bold;">7.15</span>  <span style="color: #2e8b57; font-weight: bold;">4.10</span>
 <span style="color: #2e8b57; font-weight: bold;">8.72</span>  <span style="color: #2e8b57; font-weight: bold;">5.17</span>  <span style="color: #2e8b57; font-weight: bold;">2.41</span> <span style="color: #2e8b57; font-weight: bold;">10.93</span> <span style="color: #2e8b57; font-weight: bold;">14.08</span>  <span style="color: #2e8b57; font-weight: bold;">5.44</span>  <span style="color: #2e8b57; font-weight: bold;">2.81</span>  <span style="color: #2e8b57; font-weight: bold;">3.99</span>  <span style="color: #2e8b57; font-weight: bold;">8.40</span> <span style="color: #2e8b57; font-weight: bold;">11.28</span>  <span style="color: #2e8b57; font-weight: bold;">3.97</span> <span style="color: #2e8b57; font-weight: bold;">18.99</span> <span style="color: #2e8b57; font-weight: bold;">10.51</span>
 <span style="color: #2e8b57; font-weight: bold;">9.52</span>  <span style="color: #2e8b57; font-weight: bold;">2.01</span>  <span style="color: #2e8b57; font-weight: bold;">1.5</span>
;
&nbsp;
<span style="color: #0000ff;">%let</span> <span style="color: #0000ff;">DSName</span>  = Bulkhead;
<span style="color: #0000ff;">%let</span> <span style="color: #0000ff;">varName</span> = Position;</pre></td></tr></table></div>




<p> You can use either PROC UNIVARIATE or PROC KDE to fit a kernel density estimate.
I will use PROC KDE because, as we will see in the next section, it supports options to truncate
the KDE.
The following call fits the KDE to the data and visualizes the result. The UNISTATS option displays descriptive statistics for the
data and shows the bandwidth chosen for the fit:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #000080; font-weight: bold;">proc kde</span> <span style="color: #000080; font-weight: bold;">data</span>=&amp;<span style="color: #0000ff;">DSName</span>;
   univar &amp;<span style="color: #0000ff;">varName</span> / unistats;
<span style="color: #000080; font-weight: bold;">run</span>;</pre></td></tr></table></div>





<img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc1.png" alt="" width="214" height="405" class="alignnone size-full wp-image-59369" srcset="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc1.png 214w, https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc1-159x300.png 159w" sizes="(max-width: 214px) 100vw, 214px" />
<br />

<figure id="attachment_59372" aria-describedby="caption-attachment-59372" style="width: 480px" class="wp-caption alignnone"><a href="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc2.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc2.png" alt="" width="480" height="360" class="size-full wp-image-59372" srcset="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc2.png 640w, https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc2-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a><figcaption id="caption-attachment-59372" class="wp-caption-text">Original Untruncated KDE</figcaption></figure>

<p>
I have highlighted a few entries in the output tables. Although the range of the data is [1.5, 21.28], 
the Controls table shows that the range of the KDE is [-4.99, 27.77].
The graph of the KDE curve confirms this fact. 
The KDE extends about three bandwidths past the extremes of the data, and the kernel bandwidth
for these data is 2.13.
However, it does not make sense to estimate the 
density to be nonzero for Position &lt; 0 because Position must be a positive quantity.
</p>

<p>
PROC KDE provides options that enable you to truncate the density estimate in two ways:
</p>
<ul>
<li>Truncate the KDE to the range of the data by using the TRUNCATE option.</li>
<li>Truncate the KDE to an arbitrary range by using the GRIDL= option to specify a lower limit and/or
the GRIDU= option to specify an upper limit.
</li></ul>

<p>
It is somewhat unfortunate that PROC KDE does not have an option to display the histogram and KDE on the percentage scale.
Most graphs in this article are shown on the frequency scale. You can <a href="https://blogs.sas.com/content/iml/2024/06/19/scale-density-curve-histogram.html">rescale the graphs to display them on the percent or density scale.</a>
The last graph in this article is shown on the percentage scale.
</p>


<h3>Truncate the density into the data range</h3>
<p>
The following call uses the TRUNCATE option to restrict the KDE to the range of the data:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* truncate KDE to data range */</span>
<span style="color: #000080; font-weight: bold;">proc kde</span> <span style="color: #000080; font-weight: bold;">data</span>=&amp;<span style="color: #0000ff;">DSName</span>;
   univar &amp;<span style="color: #0000ff;">varName</span> / truncate out=KDE_truncData; <span style="color: #006400; font-style: italic;">/* save KDE to data set for later analysis */</span>
<span style="color: #000080; font-weight: bold;">run</span>;</pre></td></tr></table></div>





<figure id="attachment_59390" aria-describedby="caption-attachment-59390" style="width: 480px" class="wp-caption alignnone"><a href="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc3.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc3.png" alt="" width="480" height="360" class="size-full wp-image-59390" srcset="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc3.png 640w, https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc3-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a><figcaption id="caption-attachment-59390" class="wp-caption-text">Truncate KDE to Data Range</figcaption></figure>

<p>
Notice that the KDE is truncated on the interval [1.5, 21.28], which is the range of the data.
</p>

<h3>Truncate the density onto the positive half-line</h3>

<p>
The Position variable cannot be negative, but there is no intrinsic reason why it cannot be smaller than 1.5. 
There is also no obvious reason to bound the density from above.
An alternative way to truncate the density is to use the GRIDL=0 option to truncate the density to strictly positive values.
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #000080; font-weight: bold;">proc kde</span> <span style="color: #000080; font-weight: bold;">data</span>=&amp;<span style="color: #0000ff;">DSName</span>;
   univar &amp;<span style="color: #0000ff;">varName</span> / gridl=<span style="color: #2e8b57; font-weight: bold;">0</span> out=KDE_trunc0; <span style="color: #006400; font-style: italic;">/* save KDE to data set for later analysis */</span>
<span style="color: #000080; font-weight: bold;">run</span>;</pre></td></tr></table></div>




<figure id="attachment_59387" aria-describedby="caption-attachment-59387" style="width: 480px" class="wp-caption alignnone"><a href="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc4.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc4.png" alt="" width="480" height="360" class="size-full wp-image-59387" srcset="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc4.png 640w, https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc4-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a><figcaption id="caption-attachment-59387" class="wp-caption-text">Truncate KDE Below Zero</figcaption></figure>

<p>
This KDE is restricted to positive values. It is unbounded above, but the density is exactly zero for negative values of the Position variable.
If you have an upper limit, you can set it by using the GRIDU= option. For example, if you are plotting the density of student scores on an exam that has 100 as a perfect score, you could use <code class="preserve-code-formatting">GRIDL=0 GRIDU=100</code> to bound the KDE to the range [0, 100].
</p>

<h3>Limitations of the truncation method</h3>
<p>
There are two problems with truncating a KDE:
</p>
<ul>
<li>On a probability scale, the area under a true density must integrate to 1.
If you tell PROC KDE to truncate the KDE, it does so by throwing away some area.
Thus, area is less than 1 for the restricted KDE.</li>
<li>Even if the estimate is rescaled, the KDE underestimates the expected value of distribution near 0 (Silverman, 1986, Section 2.10).
</li>
</ul>

<p>
You can rescale to the density to correct for the first problem. In the previous sections, I wrote the values of the KDE to the data sets 
KDE_truncData
and
KDE_trunc0, respectively.
The following macro computes the area under these curves by using <a href="https://blogs.sas.com/content/iml/2011/07/08/the-area-under-a-density-estimate-curve-nonparametric-estimates.html">a trapezoidal approximation to the area</a>.
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* the area under a density estimate should be 1. See
   https://blogs.sas.com/content/iml/2011/07/08/the-area-under-a-density-estimate-curve-nonparametric-estimates.html
   The following macro computes the area under a KDE estimate that is created by PROC KDE and written
   to the 'DSName' data set by using the OUT= option on the UNIVAR statement.
   In the data set the x variable is named 'value', and the 'density' variable is f(x).
   */</span>
<span style="color: #0000ff;">%macro</span> AreaUnderKDE<span style="color: #66cc66;">&#40;</span>DS<span style="color: #66cc66;">&#41;</span>;
<span style="color: #000080; font-weight: bold;">data</span> <span style="color: #0000ff;">_null_</span>;
   <span style="color: #0000ff;">set</span> <span style="color: #0000ff; font-weight: bold;">&amp;DS</span> <span style="color: #0000ff;">end</span>=EOF;
   dx = <span style="color: #0000ff;">dif</span><span style="color: #66cc66;">&#40;</span>value<span style="color: #66cc66;">&#41;</span>;
   meanY = <span style="color: #66cc66;">&#40;</span>density + <span style="color: #0000ff;">lag</span><span style="color: #66cc66;">&#40;</span>density<span style="color: #66cc66;">&#41;</span><span style="color: #66cc66;">&#41;</span> / <span style="color: #2e8b57; font-weight: bold;">2</span>;
   Area + dx<span style="color: #006400; font-style: italic;">*meanY;</span>
   <span style="color: #0000ff;">if</span> EOF <span style="color: #0000ff;">then</span> <span style="color: #0000ff;">do</span>;
      <span style="color: #0000ff;">call</span> symputx<span style="color: #66cc66;">&#40;</span><span style="color: #a020f0;">'Area'</span>, Area, <span style="color: #a020f0;">&quot;G&quot;</span><span style="color: #66cc66;">&#41;</span>; <span style="color: #006400; font-style: italic;">/* create global macro */</span>
   <span style="color: #0000ff;">end</span>;
<span style="color: #000080; font-weight: bold;">run</span>;
<span style="color: #0000ff;">%mend</span>;
&nbsp;
%AreaUnderKDE<span style="color: #66cc66;">&#40;</span>KDE_truncData<span style="color: #66cc66;">&#41;</span>;
<span style="color: #0000ff;">%put</span> <span style="color: #0000ff;">Trunc</span> <span style="color: #000080; font-weight: bold;">Data</span> &amp;=Area;
%AreaUnderKDE<span style="color: #66cc66;">&#40;</span>KDE_trunc0<span style="color: #66cc66;">&#41;</span>;
<span style="color: #0000ff;">%put</span> <span style="color: #0000ff;">Trunc</span> to <span style="color: #2e8b57; font-weight: bold;">0</span> &amp;=Area;</pre></td></tr></table></div>





<div class="wp_syntax"><table><tr><td class="code"><pre class="text" style="font-family:monospace;">Trunc Data AREA=0.9031115838
Trunc to 0 AREA=0.97522813</pre></td></tr></table></div>




<p>
The log shows that the area is less than 1 for both KDE curves.
This can be a problem if you intend to use the KDE to estimate probabilities.
However, it is simple enough to rescale the truncated density by 
dividing it by its area:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* If 'density' is a variable that does not integrates to 1,
   you can renormalize by dividing by the Area under the density curve. */</span>
<span style="color: #0000ff;">%macro</span> Renormalize<span style="color: #66cc66;">&#40;</span>DS, Area<span style="color: #66cc66;">&#41;</span>;
   <span style="color: #000080; font-weight: bold;">data</span> <span style="color: #0000ff; font-weight: bold;">&amp;DS</span>;
      <span style="color: #0000ff;">set</span> <span style="color: #0000ff; font-weight: bold;">&amp;DS</span>;
      density = density / <span style="color: #0000ff; font-weight: bold;">&amp;Area</span>;
   <span style="color: #000080; font-weight: bold;">run</span>;
<span style="color: #0000ff;">%mend</span>;
&nbsp;
%Renormalize<span style="color: #66cc66;">&#40;</span>KDE_trunc0, <span style="color: #0000ff; font-weight: bold;">&amp;Area</span><span style="color: #66cc66;">&#41;</span>;</pre></td></tr></table></div>




<p>The following graph overlays the renormalized truncated KDE on a histogram and fringe plot of the data. 
The KDE is truncated at 0 because the measured quantity must be positive. The graph was created by using the 
<a href="https://blogs.sas.com/content/iml/2025/10/13/high-low-emulate-histogram.html">%EmulateHistogram macro</a>
and the process described in the article, 
<a href="https://blogs.sas.com/content/iml/2025/10/20/overlay-curves-histogram.html">"Overlay multiple custom density curves on a histogram in SAS."</a>
</p>

<figure id="attachment_59402" aria-describedby="caption-attachment-59402" style="width: 640px" class="wp-caption alignnone"><a href="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc5.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc5.png" alt="" width="480" height="360" class="size-full wp-image-59402" srcset="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc5.png 640w, https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc5-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a><figcaption id="caption-attachment-59402" class="wp-caption-text">Truncated and Renormalized KDE</figcaption></figure>

<p>
The second issue (underestimating the KDE near the boundary) does not have a simple solution. Some researchers suggest reflecting the data 
across x=0 before fitting the KDE. I don't want to say more about that method here, but <a href="https://blogs.sas.com/content/iml/2012/04/06/creating-a-periodic-smoother.html">I have used it previously to create smoothers of periodic data.</a>
</p>

<h3>Summary</h3>
<p>
If you naively compute a kernel density estimate for data that are inherently positive, the KDE might estimate density 
for negative values of the variable. 
PROC KDE in SAS provides two methods for correcting this problem:
</p>
<ul>
<li>The TRUNCATE option on the UNIVAR statement truncates the KDE to the range of the data.</li>
<li>The GRIDL=L and GRIDU=U options truncate the KDE to the range [L, U]. For example, you can use GRIDL=0 to truncate the KDE onto the positive axis.</li>
</ul>

<p>
Technically, if you truncate the KDE, you should renormalize it so that the area under the curve is unity. 
This article includes two macros (%AreaUnderKDE and %Renormalize) that you can use for that purpose.
I do not deal with the fact that the truncated KDE underestimates the expected density near the boundary.
In a subsequent article, I present an alternative method: log-transforming the data.
</p>
<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/07/13/kde-positive.html">Kernel density estimates for positive data</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blogs.sas.com/content/iml/2026/07/13/kde-positive.html/feed</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			<enclosure url="https://blogs.sas.com/content/iml/files/2026/07/KDEtrunc5-150x150.png" />
	</item>
		<item>
		<title>An alternative Pareto chart in SAS</title>
		<link>https://blogs.sas.com/content/iml/2026/07/06/alt-pareto-chart.html</link>
					<comments>https://blogs.sas.com/content/iml/2026/07/06/alt-pareto-chart.html#respond</comments>
		
		<dc:creator><![CDATA[Rick Wicklin]]></dc:creator>
		<pubDate>Mon, 06 Jul 2026 09:27:25 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Statistical Graphics]]></category>
		<category><![CDATA[Statistical Programming]]></category>
		<guid isPermaLink="false">https://blogs.sas.com/content/iml/?p=38138</guid>

					<description><![CDATA[<p>Pareto charts are used by quality engineers to visually display a set of causes that produce defects in a manufacturing process. The chart is based on the famous Pareto Principle (the 80/20 rule), which states that 20% of the causes often produce 80% of the defects. If you focus on [...]</p>
<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/07/06/alt-pareto-chart.html">An alternative Pareto chart in SAS</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>
Pareto charts are used by quality engineers to visually display a set of causes that produce defects in a manufacturing process.
The chart is based on the famous Pareto Principle (the 80/20 rule),
which states that 20% of the causes often produce 80% of the defects. 
If you focus on the most common causes, you can quickly reduce your defect count.
</p>
<p>
This article describes an alternative to 
<a href="https://blogs.sas.com/content/iml/2026/06/22/pareto-charts-sas.html">the standard Pareto chart</a>
and shows how to create the alternative version in SAS. 
</p>

<h3>A problem with the standard Pareto chart</h3>

<a href="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart2.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart2.png" alt="" width="320" height="240" class="alignright size-full wp-image-59158" srcset="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart2.png 640w, https://blogs.sas.com/content/iml/files/2026/06/ParetoChart2-300x225.png 300w" sizes="(max-width: 320px) 100vw, 320px" /></a>

<p>
Not every situation obeys the 80-20 rule. For your process, you might find that 80% of the defects are produced by 33% of the causes. 
Or, maybe 75% of the defects are produced by 10% of the causes.
If your process is modeled by a Pareto distribution (a specific power-law distribution),
you can <a href="https://blogs.sas.com/content/iml/2018/11/05/fit-pareto-distribution-sas.html">use SAS to fit your data to a Pareto distribution</a>. However, most practitioners do not do that. They just display a bar chart of the causes where the bars are displayed in descending order and then 
try to address the most frequent causes.  For example, if a quality engineer looks at the Pareto chart to the right, she might decide to address the "contamination" cause first, followed by the "oxide defect" issue. Together, these two categories account for 67.5% of the defects.
</p>
<p>
But there is a fundamental problem with this strategy.  In small samples, the empirical frequencies in the data might be 
much different than the underlying probabilities for the process.
Thus, the "most frequent" category in the data might not be the most probable cause of the defects.
It would be nice to know whether the tall bars to the left 
just happen to be tall because of random variation or whether they are actually the most probable categories
and warrant your attention.
</p><p>
In a short three-page paper, <a href="https://doi.org/10.1198/000313006X152243">Wilkinson (2006, TAS, "Revising the Pareto Chart")</a>
proposes an alternative to the Pareto chart that uses the idea of statistical significance to 
give additional insight into the tallest bars.
The Wilkinson Pareto chart provides <em>acceptance intervals</em> that indicate 
whether each category appears more often than would be expected if all categories are equally probable. 
This enables you to assess whether the tallest bars are "sufficiently tall" and are, in fact, more 
probable than the other bars.
</p>

<h3>A Pareto chart with acceptance intervals</h3>
<p>
If you have a license for SAS/QC software, you can create Wilkinson's alternative Pareto chart by using the CHARTTYPE=INTERVALS option on the VBAR statement in PROC PARETO. 
<a href="https://blogs.sas.com/content/iml/2026/06/22/pareto-charts-sas.html">A previous article showed how to create two standard Pareto charts in SAS.</a>  Let's analyze the same data set, which contains the causes of 40 randomly selected defective components in a manufacturing process.
The following DATA step defines the data. The call to PROC PARETO creates Wilkinson's acceptance-interval chart:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* Modification of PROC PARETO example data */</span>
<span style="color: #000080; font-weight: bold;">data</span> Failure;
<span style="color: #0000ff;">INFILE</span> DATALINES delimiter=<span style="color: #a020f0;">','</span>;
<span style="color: #0000ff;">length</span> Cause $ <span style="color: #2e8b57; font-weight: bold;">16</span>;
<span style="color: #0000ff;">label</span> Cause = <span style="color: #a020f0;">'Cause of Failure'</span>;
<span style="color: #0000ff;">input</span> Cause @@;
datalines;
Corrosion, Oxide Defect, Contamination, Oxide Defect
Oxide Defect, Oxide Defect, Contamination, Metallization
Oxide Defect, Contamination, Contamination, Oxide Defect
Contamination, Contamination, Contamination, Corrosion
Silicon Defect, Contamination, Contamination, Contamination
Contamination, Contamination, Doping, Oxide Defect
Oxide Defect, Metallization, Contamination, Corrosion
Silicon Defect, Contamination, Corrosion, Corrosion
Metallization, Oxide Defect, Contamination, Contamination
Oxide Defect, Doping, Doping, Contamination
;
&nbsp;
<span style="color: #0000ff;">%let</span> <span style="color: #0000ff;">DSName</span> = Failure;
<span style="color: #0000ff;">%let</span> <span style="color: #0000ff;">VarName</span> = Cause;
&nbsp;
<span style="color: #006400; font-style: italic;">/* Wilkinson variant of Pareto chart. Which categories are 
   significantly different from a uniform distribution of 
   causes? */</span>
<span style="color: #000080; font-weight: bold;">proc pareto</span> <span style="color: #000080; font-weight: bold;">data</span>=&amp;<span style="color: #0000ff;">DSName</span>;
   vbar &amp;<span style="color: #0000ff;">VarName</span> / charttype=intervals;
<span style="color: #000080; font-weight: bold;">run</span>;</pre></td></tr></table></div>





<a href="https://blogs.sas.com/content/iml/files/2026/06/ParetoAlt1.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/ParetoAlt1.png" alt="" width="480" height="360" class="alignnone size-full wp-image-59303" srcset="https://blogs.sas.com/content/iml/files/2026/06/ParetoAlt1.png 640w, https://blogs.sas.com/content/iml/files/2026/06/ParetoAlt1-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a>

<p>
What exactly are we looking at? <a href="https://go.documentation.sas.com/doc/en/pgmsascdc/v_073/qcug/qcug_pareto_examples13.htm">The documentation for PROC PARETO</a> states,
"the most frequently occurring problem, Contamination, occurs more frequently
than the first-ranked cause from a random sample of ... uniformly distributed causes. This result indicates
that addressing contamination problems should be given a high priority."
</p><p>
The documentation does not explain the term "first-ranked cause" or the connection to "uniformly distributed causes."
</p>

<h3>Understanding the Pareto chart with acceptance intervals</h3>
<p>
The chart shows both the observed percentages and the expected percentages under the null hypothesis that all categories are equally likely to occur.
However, the fact that a Pareto chart is sorted, introduces a complication into what would otherwise be a simple process.
</p><p>
This example has 6 categories. If the categories are equally likely, we would expect each bar in a bar chart 
to contain about 16.67% of the defects.
In the observed sample, the most frequent category (Contamination) contains 42.5% of the defects. Intuitively, we suspect that 42.5% is so much greater than 16.67% that this number 
is unlikely to be observed by chance if the categories are equally likely. On the other hand,
the second most frequent category (Oxide Defect) contains 25% of the defects, which is not too much larger than 16.67%.
Perhaps that 
category is no more likely than any of the other remaining categories? 
</p><p>
Wilkinson's chart visualizes the comparison between the observed percentages and the null hypothesis where every category is equally likely. 
For each category, the graph shows two features:
</p>
<ul>
<li>
The dots show the observed percentage of defects in each category. Thus, the first category (Contamination) shows a dot at 42.5%,
the second shows a dot at 25%, the third shows a dot at 12.5%, and so forth.
</li>
<li>
The bars represent 95% confidence intervals <em>under the null hypothesis</em> that all six causes are equally probable and under the assumption that the bars are being displayed in decreasing order of size.
Wilkinson calls these the <em>acceptance regions</em>.
If the observed percentage is outside and above the acceptance region (as it is for the Contamination category), it indicates that the 
most-frequent category is observed significantly more often than would be expected for the most-frequent category under the null scenario.
If the observed percentage is inside the acceptance region (for example, Oxide Defect, 
Metallization, and Silicon Defect), it indicates that the 
observed frequency is not different from what would be expected under the null scenario.
If the observed percentage is below the acceptance region (Corrosion and Doping),
the observed frequency is less than what would be expected under the null scenario.
</li>
</ul>

<p>
When I first saw this plot, I was a little confused because I expected the acceptance regions to be centered around 16.7%, 
which is the expected value for six equally probable categories. However, they are not!
The reason for this discrepancy is that the acceptance regions represent the 
range of 95% of the <em>ranked</em> categories under the hypothesis that all categories are equally probable.
</p><p>
If all categories were equally probable, random variation 
causes some categories to have more observations than others. 
When you sort the categories, the first-ranked bar is almost always taller than 16.7%.
Similarly, the last-ranked bar is almost always less than 16.7%. Thus, you must adjust the heights of the 
acceptance regions to account for the fact that the categories are shown in decreasing order.
</p>

<h3>Using simulation to create the acceptance intervals</h3>
<p>
You can use simulation to create the acceptance intervals, which are the ranges of the 
first-ranked, second-ranked, and third-ranked (etc.) categories when the 
categories are selected uniformly at random from a set of size N. In this example, N=40 
and there are K=6 categories. 
You can use the RANDMULTINOMIAL function in SAS IML software to simulate a large number (B=5000) of samples.
For each sample, sort the counts in descending order to mimic the method used by the Pareto chart.
You can do these two steps efficiently by simulating all B samples into a matrix that has B rows and K columns.
You then <a href="https://blogs.sas.com/content/iml/2026/06/24/sort-rows-cols-matrix.html">sort each row independently</a>.
</p><p>
After this procedure, the first column of the sorted matrix contains the distribution of counts for the first-ranked category
in each sample.
In general, the i_th column contains the distribution of counts for the i_th-ranked category.
You can therefore compute the 2.5th and 97.5 percentiles to form a 95% confidence interval for the counts.
This procedure is implemented in the following IML program:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #000080; font-weight: bold;">proc iml</span>;
<span style="color: #0000ff;">call</span> randseed<span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">1234</span><span style="color: #66cc66;">&#41;</span>;
<span style="color: #0000ff;">N</span> = <span style="color: #2e8b57; font-weight: bold;">40</span>;                <span style="color: #006400; font-style: italic;">/* Total number of observations */</span>
K = <span style="color: #2e8b57; font-weight: bold;">6</span>;                 <span style="color: #006400; font-style: italic;">/* Number of categories */</span>
alpha = <span style="color: #2e8b57; font-weight: bold;">0.05</span>;          <span style="color: #006400; font-style: italic;">/* Significance level */</span>
B = <span style="color: #2e8b57; font-weight: bold;">5000</span>;              <span style="color: #006400; font-style: italic;">/* Number of Monte Carlo iterations */</span>
&nbsp;
<span style="color: #006400; font-style: italic;">/* Monte Carlo simulation from the Null distribution, which assumes uniform probability */</span>
prob = j<span style="color: #66cc66;">&#40;</span>K, <span style="color: #2e8b57; font-weight: bold;">1</span>, <span style="color: #2e8b57; font-weight: bold;">1</span>/K<span style="color: #66cc66;">&#41;</span>;
Y = RandMultinomial<span style="color: #66cc66;">&#40;</span>B, <span style="color: #0000ff;">N</span>, prob<span style="color: #66cc66;">&#41;</span>;
<span style="color: #006400; font-style: italic;">/* Sort each row. Columns contain frequencies for each rank (1st, 2nd, 3rd,...).
   You could also use the SortMat function; see 
   https://blogs.sas.com/content/iml/2026/06/24/sort-rows-cols-matrix.html
*/</span>
<span style="color: #0000ff;">do</span> i = <span style="color: #2e8b57; font-weight: bold;">1</span> to nrow<span style="color: #66cc66;">&#40;</span>Y<span style="color: #66cc66;">&#41;</span>;
   v = colvec<span style="color: #66cc66;">&#40;</span>Y<span style="color: #66cc66;">&#91;</span>i,<span style="color: #66cc66;">&#93;</span><span style="color: #66cc66;">&#41;</span>;
   <span style="color: #0000ff;">call</span> sort<span style="color: #66cc66;">&#40;</span>v, <span style="color: #2e8b57; font-weight: bold;">1</span>, <span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#41;</span>; <span style="color: #006400; font-style: italic;">/* sort descending */</span>
   Y<span style="color: #66cc66;">&#91;</span>i, <span style="color: #66cc66;">&#93;</span> = rowvec<span style="color: #66cc66;">&#40;</span>v<span style="color: #66cc66;">&#41;</span>;
<span style="color: #0000ff;">end</span>;
w = <span style="color: #66cc66;">&#40;</span>alpha/<span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#41;</span> // <span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">1</span>-alpha/<span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#41;</span>;
<span style="color: #0000ff;">call</span> qntl<span style="color: #66cc66;">&#40;</span>CL, Y, w<span style="color: #66cc66;">&#41;</span>;              <span style="color: #006400; font-style: italic;">/* 95% CL of counts for ranked categories */</span>
<span style="color: #006400; font-style: italic;">/* transpose CL to make it easier to visualize */</span>
CL = CL`;
print CL<span style="color: #66cc66;">&#91;</span>c=<span style="color: #66cc66;">&#123;</span><span style="color: #a020f0;">'LowerCL'</span> <span style="color: #a020f0;">'UpperCL'</span><span style="color: #66cc66;">&#125;</span> r=<span style="color: #66cc66;">&#40;</span><span style="color: #a020f0;">'R1'</span>:<span style="color: #a020f0;">'R6'</span><span style="color: #66cc66;">&#41;</span><span style="color: #66cc66;">&#93;</span>;
<span style="color: #000080; font-weight: bold;">QUIT</span>;</pre></td></tr></table></div>





<img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/ParetoAlt2.png" alt="" width="176" height="231" class="alignnone size-full wp-image-59297" />

<p>
The output shows the confidence intervals for counts when a set of K=6 categories are chosen uniformly at random (with replacement) N=40 times.
The first-ranked category (the most-frequent one) usually has 8-13 counts.
This is larger than the 40/6 = 6.67 counts that you might have expected.
Similarly, the second-ranked category usually has between 7-10 counts. 
The last two categories usually have 6 or fewer counts. 
Obviously, you can divide these counts by N=40 to get the corresponding percentages.
You can then write the percentages to a SAS data set and use a HIGHLOW statement in PROC SGPLOT to reproduce Wilkinson's Pareto chart.
</p>

<h3>A SAS macro to create acceptance regions for a Pareto chart</h3>
<p>
If you have a license for SAS IML software, the  following SAS macro computes the 
same alternative Pareto chart as the CHARTTYPE=INTERVALS option in PROC PARETO.
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* Plot the observed counts as a dot plot and overlay an acceptance band
   composed of the lower and upper 95% limits for the null distribution.
   This macro requires license for SAS IML.
   EXAMPLES:
   %AcceptancePareto(sashelp.cars, Type);   * 95% acceptance regions;
   %AcceptancePareto(sashelp.cars, Cylinders, alpha=0.1);   * 90% acceptance regions;
*/</span>
<span style="color: #0000ff;">%macro</span> AcceptancePareto<span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">DSName</span>, <span style="color: #0000ff;">VarName</span>, alpha=<span style="color: #2e8b57; font-weight: bold;">0.05</span><span style="color: #66cc66;">&#41;</span>;
   <span style="color: #0000ff;">%local</span> conf;
   <span style="color: #0000ff;">%let</span> conf = <span style="color: #0000ff;">%sysevalf</span><span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">100</span><span style="color: #006400; font-style: italic;">*(1-&amp;alpha));</span>
&nbsp;
   <span style="color: #000080; font-weight: bold;">proc freq</span> <span style="color: #000080; font-weight: bold;">data</span>=&amp;<span style="color: #0000ff;">DSName</span> <span style="color: #0000ff;">order</span>=Freq noprint;
      <span style="color: #0000ff;">where</span> <span style="color: #0000ff;">not</span> <span style="color: #0000ff;">missing</span><span style="color: #66cc66;">&#40;</span>&amp;<span style="color: #0000ff;">VarName</span><span style="color: #66cc66;">&#41;</span>;
      <span style="color: #0000ff;">table</span> &amp;<span style="color: #0000ff;">VarName</span> / out=_FreqOut outcum;
   <span style="color: #000080; font-weight: bold;">run</span>;
&nbsp;
   <span style="color: #006400; font-style: italic;">/* compute upper and lower CL */</span>
   <span style="color: #000080; font-weight: bold;">proc iml</span>;
   <span style="color: #0000ff;">call</span> randseed<span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">1234</span><span style="color: #66cc66;">&#41;</span>;
   use _FreqOut;   read all <span style="color: #0000ff;">var</span> <span style="color: #a020f0;">&quot;count&quot;</span>;   <span style="color: #0000ff;">close</span>;
&nbsp;
   <span style="color: #0000ff;">N</span> = <span style="color: #0000ff;">sum</span><span style="color: #66cc66;">&#40;</span>count<span style="color: #66cc66;">&#41;</span>;                <span style="color: #006400; font-style: italic;">/* Total number of observations */</span>
   K = nrow<span style="color: #66cc66;">&#40;</span>count<span style="color: #66cc66;">&#41;</span>;               <span style="color: #006400; font-style: italic;">/* Number of categories */</span>
   alpha = <span style="color: #0000ff; font-weight: bold;">&amp;alpha</span>;                <span style="color: #006400; font-style: italic;">/* Significance level */</span>
   B = <span style="color: #2e8b57; font-weight: bold;">5000</span>;                      <span style="color: #006400; font-style: italic;">/* Number of Monte Carlo iterations */</span>
&nbsp;
   <span style="color: #006400; font-style: italic;">/* Monte Carlo simulation from the Null distribution, which assumes uniform probability */</span>
   prob = j<span style="color: #66cc66;">&#40;</span>K, <span style="color: #2e8b57; font-weight: bold;">1</span>, <span style="color: #2e8b57; font-weight: bold;">1</span>/K<span style="color: #66cc66;">&#41;</span>;  <span style="color: #006400; font-style: italic;">/* H0: All categories are equally probable */</span>
   Y = RandMultinomial<span style="color: #66cc66;">&#40;</span>B, <span style="color: #0000ff;">N</span>, prob<span style="color: #66cc66;">&#41;</span>;
   <span style="color: #006400; font-style: italic;">/* Sort each row. Columns contain frequencies for each rank (1st, 2nd, 3rd,...) */</span>
   <span style="color: #0000ff;">do</span> i = <span style="color: #2e8b57; font-weight: bold;">1</span> to nrow<span style="color: #66cc66;">&#40;</span>Y<span style="color: #66cc66;">&#41;</span>;
      v = colvec<span style="color: #66cc66;">&#40;</span>Y<span style="color: #66cc66;">&#91;</span>i,<span style="color: #66cc66;">&#93;</span><span style="color: #66cc66;">&#41;</span>;
      <span style="color: #0000ff;">call</span> sort<span style="color: #66cc66;">&#40;</span>v, <span style="color: #2e8b57; font-weight: bold;">1</span>, <span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#41;</span>; <span style="color: #006400; font-style: italic;">/* sort descending */</span>
      Y<span style="color: #66cc66;">&#91;</span>i, <span style="color: #66cc66;">&#93;</span> = rowvec<span style="color: #66cc66;">&#40;</span>v<span style="color: #66cc66;">&#41;</span>;
   <span style="color: #0000ff;">end</span>;
   w = <span style="color: #66cc66;">&#40;</span>alpha/<span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#41;</span> // <span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">1</span>-alpha/<span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#41;</span>;
   <span style="color: #0000ff;">call</span> qntl<span style="color: #66cc66;">&#40;</span>CL, Y, w<span style="color: #66cc66;">&#41;</span>;              <span style="color: #006400; font-style: italic;">/* 95% CL of counts for ranked categories */</span>
   <span style="color: #006400; font-style: italic;">/* transpose CL and convert to percentage */</span>
   CL = <span style="color: #2e8b57; font-weight: bold;">100</span> <span style="color: #006400; font-style: italic;">* CL` / N;</span>
   <span style="color: #0000ff;">create</span> _CL <span style="color: #0000ff;">from</span> CL<span style="color: #66cc66;">&#91;</span>c=<span style="color: #66cc66;">&#123;</span><span style="color: #a020f0;">'_Lower'</span> <span style="color: #a020f0;">'_Upper'</span><span style="color: #66cc66;">&#125;</span><span style="color: #66cc66;">&#93;</span>;
      append <span style="color: #0000ff;">from</span> CL;
   <span style="color: #0000ff;">close</span>;
   <span style="color: #000080; font-weight: bold;">QUIT</span>;
&nbsp;
   <span style="color: #000080; font-weight: bold;">data</span> _AcceptancePareto;
   <span style="color: #0000ff;">merge</span> _FreqOut _CL; 
   <span style="color: #0000ff;">label</span> _Lower = <span style="color: #a020f0;">&quot;Lower &amp;conf.% Acceptance Percent&quot;</span>
         _Upper = <span style="color: #a020f0;">&quot;Upper &amp;conf.% Acceptance Percent&quot;</span>
         Percent = <span style="color: #a020f0;">&quot;Observed Percent&quot;</span>;
   <span style="color: #000080; font-weight: bold;">run</span>;
&nbsp;
   <span style="color: #000080; font-weight: bold;">proc sgplot</span> <span style="color: #000080; font-weight: bold;">data</span>=_AcceptancePareto;
      highlow <span style="color: #0000ff;">x</span>=&amp;<span style="color: #0000ff;">VarName</span> low=_Lower high=_Upper / 
              type=bar barwidth=<span style="color: #2e8b57; font-weight: bold;">0.8</span> legendlabel=<span style="color: #a020f0;">&quot;&amp;conf.% Acceptance Interval&quot;</span>;
      scatter <span style="color: #0000ff;">x</span>=&amp;<span style="color: #0000ff;">VarName</span> y=Percent;
      yaxis  <span style="color: #0000ff;">min</span>=<span style="color: #2e8b57; font-weight: bold;">0</span> offsetmin=<span style="color: #2e8b57; font-weight: bold;">0</span> grid <span style="color: #0000ff;">label</span>=<span style="color: #a020f0;">&quot;Percent&quot;</span>;
      xaxis type=discrete discreteorder=<span style="color: #000080; font-weight: bold;">data</span>;
      keylegend / location=Inside position=NE opaque across=<span style="color: #2e8b57; font-weight: bold;">1</span>;
      <span style="color: #0000ff;">format</span> Percent best4.;
   <span style="color: #000080; font-weight: bold;">run</span>;
<span style="color: #0000ff;">%mend</span>;
&nbsp;
<span style="color: #0000ff;">title</span> <span style="color: #a020f0;">&quot;Revised Pareto Chart with 95% Acceptance Bands&quot;</span>;
title2 <span style="color: #a020f0;">&quot;H0: Uniform Probability for Causes&quot;</span>;
<span style="color: #0000ff;">footnote</span> J=L <span style="color: #a020f0;">&quot;Wilkinson (2006, TAS)&quot;</span>;
%AcceptancePareto<span style="color: #66cc66;">&#40;</span>Failure, Cause<span style="color: #66cc66;">&#41;</span>;</pre></td></tr></table></div>





<a href="https://blogs.sas.com/content/iml/files/2026/06/ParetoAlt3.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/ParetoAlt3.png" alt="" width="480" height="360" class="alignnone size-full wp-image-59300" srcset="https://blogs.sas.com/content/iml/files/2026/06/ParetoAlt3.png 640w, https://blogs.sas.com/content/iml/files/2026/06/ParetoAlt3-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a>

<p>
I've added grid lines and a legend, but the information in this plot is the same as the one produced by PROC PARETO.
The graph shows that the empirical probability of the Contamination defects is significantly different from what would be expected
if the defect causes were equally probable. Thus, the quality engineer should strive to reduce the contamination defects.
The observed count for Oxide Defect is not significantly higher than expected. It is on the boundary of the acceptance region, 
so the engineer might want to gather more data before dedicating resources to address the Oxide Defect.
</p>

<h3>Summary</h3>
<p>
This article shows how to create an alternative to the standard Pareto charts (Wilkinson, 2006).
The alternative chart provides <em>acceptance intervals</em> that indicate 
whether each category appears more often than would be expected if all categories are equally probable. 
This enables you to assess whether the tallest bars are "sufficiently tall" and are, in fact, occurring more often 
than chance. If not, 
there is little reason to spend time and money trying to address that cause.
</p><p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/07/06/alt-pareto-chart.html">An alternative Pareto chart in SAS</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blogs.sas.com/content/iml/2026/07/06/alt-pareto-chart.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<enclosure url="https://blogs.sas.com/content/iml/files/2026/06/ParetoAlt3-150x150.png" />
	</item>
		<item>
		<title>Confidence limits for counts in a multinomial distribution</title>
		<link>https://blogs.sas.com/content/iml/2026/06/29/confidence-limits-multinomial.html</link>
					<comments>https://blogs.sas.com/content/iml/2026/06/29/confidence-limits-multinomial.html#respond</comments>
		
		<dc:creator><![CDATA[Rick Wicklin]]></dc:creator>
		<pubDate>Mon, 29 Jun 2026 09:23:49 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Simulation]]></category>
		<category><![CDATA[Statistical Programming]]></category>
		<guid isPermaLink="false">https://blogs.sas.com/content/iml/?p=59246</guid>

					<description><![CDATA[<p>A recent article discussed Pareto charts and how to create them in SAS. A Pareto chart is used in quality control to identify the most likely reasons that a manufacturing process produces defective items. Suppose there are k different reasons why a process can fail, and you intend to randomly [...]</p>
<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/06/29/confidence-limits-multinomial.html">Confidence limits for counts in a multinomial distribution</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>
A recent article discussed Pareto charts and how to create them in SAS.
A Pareto chart is used in quality control to identify the most likely reasons that 
a manufacturing process produces defective items.
Suppose there are <em>k</em> different reasons why a process can fail, and you intend to 
randomly select <em>N</em> defective items to examine.
The number of defective items for each reason can be modeled by using a <a href="https://blogs.sas.com/content/iml/2015/10/02/balls-and-urns2.html">multinomial distribution</a>.
</p><p>
A variation of the Pareto chart (<a href="https://www.tandfonline.com/doi/abs/10.1198/000313006X152243">Wilkinson, <em>TAS</em>, 2006</a>) uses
confidence intervals for multinomial counts
to enhance the information on the chart.
This article supplies the necessary background for 
how to estimate confidence intervals for multinomial counts. A subsequent article shows how to construct Wilkinson's Pareto chart in SAS.
</p>

<h3>The multinomial distribution</h3>
<p>
Suppose a categorical variable, X, can take on <em>k</em> different values.
A statistical model of the variable is to assume that X can be modeled by a multinomial distribution.
A multinomial distribution assumes that
the probability of the i_th value is <em>p<sub>i</sub></em>, where &Sigma;<sub>i</sub> <em>p<sub>i</sub></em> = 1.
</p><p>
In a sample of size <em>N</em>, there will be <em>N</em><sub>1</sub> items of the first type,
<em>N</em><sub>2</sub> items of the second type, and so forth, where &Sigma;<sub>i</sub> <em>N<sub>i</sub></em> = <em>N</em>. 
The <em>k</em>-tuple of counts
(<em>N</em><sub>1</sub>, <em>N</em><sub>2</sub>, ..., <em>N</em><sub><em>k</em></sub>) is a single observation from the multinomial distribution.  
</p><p>
The expected value of <em>N</em><sub><em>i</em></sub> is <em>N</em>*<em>p</em><sub><em>i</em></sub>,
but in an actual sample the count could be higher or lower than that value.
Confidence intervals enable you to answer the question, "what is the likely range of each count?"
</p><p>
A previous article describes <a href="https://blogs.sas.com/content/iml/2017/02/15/confidence-intervals-multinomial-proportions.html">how to construct simultaneous confidence intervals for the proportions</a>. But the Wilkinson variation of the Pareto chart
uses <em>marginal confidence limits</em>, which are different. 
Marginal confidence limits can be easily generated by running a simulation of the multinomial distribution and using percentiles of the results.
In SAS, the easiest way to simulate from the multinomial distribution is to use <a href="https://blogs.sas.com/content/iml/2013/08/05/simulate-from-multinomial-distribution.html">the RandMultinomial function in SAS IML</a>.
</p>


<h3>Simulating multinomial frequencies</h3>
<p>
Suppose a quality engineer samples 40 defective items. She finds that there are six causes for the defective items.
In the sample,  
<em>N</em><sub>1</sub>=17 items are defective because of the first cause, <em>N</em><sub>2</sub>=10 are defective because of the second cause, and the remaining causes 
are responsible for 5, 3, 3, and 2 counts, respectively.
The best estimate of the unknown probabilities is obtained by dividing the counts by <em>N</em>=40.
Thus, the empirical probabilities are 
p = {0.425, 0.25, 0.125, 0.075, 0.075, 0.05}.
</p><p>
The following call to PROC IML specifies these parameters and generates five random samples. Each sample is a random draw from the multinomial distribution.
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* CI for marginal probability parameters in a multinomial distribution */</span>
<span style="color: #000080; font-weight: bold;">proc iml</span>;
<span style="color: #006400; font-style: italic;">/* observed frequencies */</span>
freq = <span style="color: #66cc66;">&#123;</span><span style="color: #2e8b57; font-weight: bold;">17</span>, <span style="color: #2e8b57; font-weight: bold;">10</span>, <span style="color: #2e8b57; font-weight: bold;">5</span>, <span style="color: #2e8b57; font-weight: bold;">3</span>, <span style="color: #2e8b57; font-weight: bold;">3</span>, <span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#125;</span>;
<span style="color: #0000ff;">N</span> = <span style="color: #0000ff;">sum</span><span style="color: #66cc66;">&#40;</span>freq<span style="color: #66cc66;">&#41;</span>;      <span style="color: #006400; font-style: italic;">/* sample size */</span>
p = freq / <span style="color: #0000ff;">N</span>;
&nbsp;
<span style="color: #006400; font-style: italic;">/* 5 random samples of size N from Multinom(p) */</span>
<span style="color: #0000ff;">call</span> randseed<span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">123</span><span style="color: #66cc66;">&#41;</span>;
X5 = randmultinomial<span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">5</span>, <span style="color: #0000ff;">N</span>, p<span style="color: #66cc66;">&#41;</span>;
print X5<span style="color: #66cc66;">&#91;</span>c=<span style="color: #66cc66;">&#40;</span><span style="color: #a020f0;">'C1'</span>:<span style="color: #a020f0;">'C6'</span><span style="color: #66cc66;">&#41;</span> r=<span style="color: #66cc66;">&#40;</span><span style="color: #a020f0;">'S1'</span>:<span style="color: #a020f0;">'S5'</span><span style="color: #66cc66;">&#41;</span><span style="color: #66cc66;">&#93;</span>;</pre></td></tr></table></div>




<img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/multinomialCL1.png" alt="" width="251" height="250" class="alignnone size-full wp-image-59264" srcset="https://blogs.sas.com/content/iml/files/2026/06/multinomialCL1.png 251w, https://blogs.sas.com/content/iml/files/2026/06/multinomialCL1-150x150.png 150w" sizes="(max-width: 251px) 100vw, 251px" />

<p>
Each row shows the count in a random sample.
The i_th column shows the counts for the i_th defective cause across all five samples.
In this small simulation, the counts for the first cause range from 11-18, 
the counts for the second cause range from 7-11, and so forth.
Because the probabilities for the 4th-6th causes are relatively small, some samples did not contain any items
for those causes.
</p><p>
The small output gives us the intuition behind confidence intervals.
We'd like to know ranges that cover the counts for 95% of the samples.
You can do this by generating a large number of samples and calculating percentiles.
</p>


<h3>Confidence intervals for the multinomial counts</h3>
<p>
The previous output shows the range of counts for each variable for five random samples.
Now imagine generating many more random samples from the same multinomial distribution.
If you compute the 2.5th and 97.5th percentiles for each column, you obtain 
a simulation-based estimate of a 95% confidence interval for each column's values.
The following IML statements perform these calculations for a simulation that uses 10,000 random draws.
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* Use simulation to estimate (1-alpha)*100% confidence intervals 
   for counts for each category.
   - N = sample size
   - p = parameter vector for Multinomial(N, p) distribution
   - B is the number of Monte Carlo simulations
   - alpha = significance level (default 0.05) ==&gt; 95% CL   
*/</span>
start MultinomialCL<span style="color: #66cc66;">&#40;</span>B, <span style="color: #0000ff;">N</span>, p, alpha=<span style="color: #2e8b57; font-weight: bold;">0.05</span><span style="color: #66cc66;">&#41;</span>;
   <span style="color: #0000ff;">X</span> = RandMultinomial<span style="color: #66cc66;">&#40;</span>B, <span style="color: #0000ff;">N</span>, p<span style="color: #66cc66;">&#41;</span>; <span style="color: #006400; font-style: italic;">/* generate B random samples */</span>
   <span style="color: #006400; font-style: italic;">/* Find the (alpha / 2) and (1 - alpha / 2) percentiles.
      For alpha = 0.05, this estimates the 2.5th and 97.5th percentiles. */</span>
   w = <span style="color: #66cc66;">&#40;</span>alpha/<span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#41;</span> // <span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">1</span>-alpha/<span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#41;</span>;
   <span style="color: #0000ff;">call</span> qntl<span style="color: #66cc66;">&#40;</span>CL, <span style="color: #0000ff;">X</span>, w<span style="color: #66cc66;">&#41;</span>;       <span style="color: #006400; font-style: italic;">/* QNTL computes the quantiles of each column */</span>
   <span style="color: #0000ff;">return</span><span style="color: #66cc66;">&#40;</span> CL <span style="color: #66cc66;">&#41;</span>;
finish;
&nbsp;
<span style="color: #006400; font-style: italic;">/* call function and print the results in a table */</span>
limits = MultinomialCL<span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">10000</span>, <span style="color: #0000ff;">N</span>, p<span style="color: #66cc66;">&#41;</span>;
Category = <span style="color: #a020f0;">'C1'</span>:<span style="color: #a020f0;">'C6'</span>;
print limits<span style="color: #66cc66;">&#91;</span>r=<span style="color: #66cc66;">&#123;</span><span style="color: #a020f0;">'Lower95'</span> <span style="color: #a020f0;">'Upper95'</span><span style="color: #66cc66;">&#125;</span> c=Category L=<span style="color: #a020f0;">&quot;Marginal Limits (N=40)&quot;</span><span style="color: #66cc66;">&#93;</span>;</pre></td></tr></table></div>




<img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/multinomialCL2.png" alt="" width="299" height="148" class="alignnone size-full wp-image-59261" srcset="https://blogs.sas.com/content/iml/files/2026/06/multinomialCL2.png 299w, https://blogs.sas.com/content/iml/files/2026/06/multinomialCL2-164x82.png 164w" sizes="(max-width: 299px) 100vw, 299px" />

<p>
The table shows 95% confidence limits 
for the counts from a multinomial sample of size <em>N</em>=40 that has a vector of probability parameters <em>p</em>.
For the most probable category (C1), you can expect the count to be between 11 and 23 for 95% of samples.
For the least probable category (C6), you can expect the count to be between 0 and 5.
</p>
<p>
Although the table provides all relevant information, I like to graph the confidence intervals as 
bars in a chart that graphs the observed frequency and confidence intervals versus the categories, as follows:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* graph the empirical frequencies and the marginal CLs */</span>
Lower95 = limits<span style="color: #66cc66;">&#91;</span><span style="color: #2e8b57; font-weight: bold;">1</span>,<span style="color: #66cc66;">&#93;</span>;
Upper95 = limits<span style="color: #66cc66;">&#91;</span><span style="color: #2e8b57; font-weight: bold;">2</span>,<span style="color: #66cc66;">&#93;</span>;
<span style="color: #0000ff;">create</span> MultinomialLimits <span style="color: #0000ff;">var</span> <span style="color: #66cc66;">&#123;</span><span style="color: #a020f0;">'Category'</span> <span style="color: #a020f0;">'Freq'</span> <span style="color: #a020f0;">'Lower95'</span> <span style="color: #a020f0;">'Upper95'</span><span style="color: #66cc66;">&#125;</span>;
   append;
<span style="color: #0000ff;">close</span>;
<span style="color: #000080; font-weight: bold;">QUIT</span>;
&nbsp;
<span style="color: #0000ff;">title</span> <span style="color: #a020f0;">&quot;95% Confidence Limits for Frequencies&quot;</span>;
<span style="color: #000080; font-weight: bold;">proc sgplot</span> <span style="color: #000080; font-weight: bold;">data</span>=MultinomialLimits noautolegend;
   highlow <span style="color: #0000ff;">x</span>=Category low=Lower95 high=Upper95 / 
           type=bar barwidth=<span style="color: #2e8b57; font-weight: bold;">0.8</span>;<span style="color: #006400; font-style: italic;">* primary=true;</span>
   scatter <span style="color: #0000ff;">x</span>=Category y=Freq;
   yaxis offsetmin=<span style="color: #2e8b57; font-weight: bold;">0</span> grid;
   xaxis type=discrete discreteorder=<span style="color: #000080; font-weight: bold;">data</span>;
<span style="color: #000080; font-weight: bold;">run</span>;</pre></td></tr></table></div>





<a href="https://blogs.sas.com/content/iml/files/2026/06/multinomialCL3.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/multinomialCL3.png" alt="" width="480" height="360" class="alignnone size-full wp-image-59267" srcset="https://blogs.sas.com/content/iml/files/2026/06/multinomialCL3.png 640w, https://blogs.sas.com/content/iml/files/2026/06/multinomialCL3-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a>

<p>
The graph enables you to see at a glance the ranges of counts for a sample of size <em>N</em> from the multinomial(N, p) distribution.  
</p>

<h3>Summary</h3>
<p>
The statistics of small random samples can be highly variable. This article looks at the counts in a multinomial distribution.
By running a simple simulation study, you can 
construct 95% confidence intervals for the counts. This is helpful for quality engineers who need to know whether an increase or decrease in an observed count is associated with a change in the manufacturing process or is merely random variation.
These confidence intervals are used in constructing Wilkinson's variation of the Pareto chart, which I will construct in a future article.
</p>

<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/06/29/confidence-limits-multinomial.html">Confidence limits for counts in a multinomial distribution</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blogs.sas.com/content/iml/2026/06/29/confidence-limits-multinomial.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<enclosure url="https://blogs.sas.com/content/iml/files/2026/06/multinomialCL3-150x150.png" />
	</item>
		<item>
		<title>Sort the rows or columns of a matrix independently</title>
		<link>https://blogs.sas.com/content/iml/2026/06/24/sort-rows-cols-matrix.html</link>
					<comments>https://blogs.sas.com/content/iml/2026/06/24/sort-rows-cols-matrix.html#comments</comments>
		
		<dc:creator><![CDATA[Rick Wicklin]]></dc:creator>
		<pubDate>Wed, 24 Jun 2026 09:27:46 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Data Analysis]]></category>
		<category><![CDATA[SAS Programming]]></category>
		<category><![CDATA[Statistical Graphics]]></category>
		<guid isPermaLink="false">https://blogs.sas.com/content/iml/?p=59179</guid>

					<description><![CDATA[<p>When you have a data matrix, the rows represent observations and the columns represent variables. If you sort the matrix by one or more columns, the sorting occurs in a way that preserves the elements within rows. Although the rows of the sorted matrix are permuted by the sort, the [...]</p>
<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/06/24/sort-rows-cols-matrix.html">Sort the rows or columns of a matrix independently</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>
When you have a data matrix, the rows represent observations and the columns represent variables.
If you sort the matrix by one or more columns, the sorting occurs in a way that preserves the elements within rows. 
Although the rows of the sorted matrix are permuted by the sort, 
the sorted matrix is still a data matrix, but now the observations are in a different order.
</p><p>
However, there are instances in which a numerical matrix is merely a way to organize a bunch of numbers.
For example, in a simulation study, you might construct a matrix whose i_th column contains
the generated data that results from the i_th independent random sample.
In this instance, sorting each column of the matrix is useful in analyzing the sampling distribution of the 
quantiles. For example, if you sort all columns, then the first row of the sorted matrix contains the distribution of the minimum statistic,
and the last row contains the distribution of the maximum statistic.
</p>
<p>
This article shows how to sort the rows and columns of a matrix independently in the SAS IML matrix language.
</p>

<h3>A matrix from a simulation study</h3>
<p>
Suppose you want to run a simulation study to approximate the sampling distribution of quantiles in small,
normally distributed, random samples. You decide to  study samples of size N=25 and you want to graph the 
approximate distribution of the quantiles based on generating B samples from N(0,1).
</p><p>
In a matrix language like SAS IML, it is efficient to simulate all random samples by using a single 
call to the RANDFUN functions. You can fill an N&nbsp;x&nbsp;B matrix with N(0,1) variates and interpret each column as a random sample of size N.
In a real simulation study, you would choose a large number for B (such as 10,000), but for this article I will choose only B=200
samples because I want to show some visualizations by using a heat map.
The following SAS IML program generates the data for the simulation study:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #000080; font-weight: bold;">proc iml</span>;
<span style="color: #006400; font-style: italic;">/* simulation study: If X ~ N(0,1), 
   what is the distribution of the sample quantiles? */</span>
<span style="color: #0000ff;">N</span> = <span style="color: #2e8b57; font-weight: bold;">25</span>;                      <span style="color: #006400; font-style: italic;">/* sample size */</span>
B = <span style="color: #2e8b57; font-weight: bold;">200</span>;                     <span style="color: #006400; font-style: italic;">/* number of simulations of X ~ N(0,1) */</span>
<span style="color: #0000ff;">call</span> randseed<span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">1234</span><span style="color: #66cc66;">&#41;</span>;
M = randfun<span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">N</span>//B, <span style="color: #a020f0;">&quot;Normal&quot;</span><span style="color: #66cc66;">&#41;</span>; <span style="color: #006400; font-style: italic;">/* NxB matrix; each col is random sample */</span></pre></td></tr></table></div>




<p>
If you sort each column in the matrix, then it is easier to compute certain rank-based statistics and quantiles.
For example, in the sorted matrix, the first row will contain the minimum values for each sample.
Similarly, the last row contains the maximum values, and the 13th row contains the median values.
(You could also use <a href="https://blogs.sas.com/content/iml/2012/03/12/compute-sample-quantiles-by-using-the-qntl-call.html">the QNTL function</a>, which computes quantiles for each column in a matrix.)
</p>


<h3>Sorting each column of a matrix</h3>
<p>
If you use CALL SORT in IML (or the SORT procedure in Base SAS), the rows of the matrix are arranged in order according to 
one or more key columns. To sort the columns independently, you need to loop over the columns, sort each one, and then overwrite the column with the sorted version.
</p><p>
The following IML module, SortMat, can sort the columns or rows of the matrix in either ascending or descending order.
The first argument should be either "row" or "col". The second argument is the matrix to sort. The third argument is optional. If specified, use 0 to sort the rows or columns in ascending order and use 1 to sort in descending order.
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* sort the columns of a matrix independently */</span>
start SortCols<span style="color: #66cc66;">&#40;</span>M, descend=<span style="color: #2e8b57; font-weight: bold;">0</span><span style="color: #66cc66;">&#41;</span>;
  S = M;
  <span style="color: #0000ff;">if</span> ^descend <span style="color: #0000ff;">then</span> <span style="color: #0000ff;">do</span>;
     <span style="color: #0000ff;">do</span> i = <span style="color: #2e8b57; font-weight: bold;">1</span> to ncol<span style="color: #66cc66;">&#40;</span>S<span style="color: #66cc66;">&#41;</span>;
        <span style="color: #0000ff;">x</span> = S<span style="color: #66cc66;">&#91;</span>,i<span style="color: #66cc66;">&#93;</span>;
        <span style="color: #0000ff;">call</span> sort<span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">x</span><span style="color: #66cc66;">&#41;</span>;        <span style="color: #006400; font-style: italic;">/* sort each column in ascending order */</span>
        S<span style="color: #66cc66;">&#91;</span>,i<span style="color: #66cc66;">&#93;</span> = <span style="color: #0000ff;">x</span>;
     <span style="color: #0000ff;">end</span>;
  <span style="color: #0000ff;">end</span>;
  <span style="color: #0000ff;">else</span> <span style="color: #0000ff;">do</span>;
     <span style="color: #0000ff;">do</span> i = <span style="color: #2e8b57; font-weight: bold;">1</span> to ncol<span style="color: #66cc66;">&#40;</span>S<span style="color: #66cc66;">&#41;</span>;
        <span style="color: #0000ff;">x</span> = S<span style="color: #66cc66;">&#91;</span>,i<span style="color: #66cc66;">&#93;</span>;
        <span style="color: #0000ff;">call</span> sort<span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">x</span>, <span style="color: #2e8b57; font-weight: bold;">1</span>, <span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#41;</span>; <span style="color: #006400; font-style: italic;">/* sort each column in descending order */</span>
        S<span style="color: #66cc66;">&#91;</span>,i<span style="color: #66cc66;">&#93;</span> = <span style="color: #0000ff;">x</span>;
     <span style="color: #0000ff;">end</span>;
  <span style="color: #0000ff;">end</span>;
  <span style="color: #0000ff;">return</span><span style="color: #66cc66;">&#40;</span> S <span style="color: #66cc66;">&#41;</span>;
finish;
&nbsp;
<span style="color: #006400; font-style: italic;">/* sort the rows or columns of a matrix independently.
   SYNTAX:
   A_row = SortMat(&quot;row&quot;, A &lt;,descend=0&gt;)
   A_col = SortMat(&quot;col&quot;, A &lt;,descend=0&gt;)
*/</span>
start SortMat<span style="color: #66cc66;">&#40;</span>direction, M, descend=<span style="color: #2e8b57; font-weight: bold;">0</span><span style="color: #66cc66;">&#41;</span>;
  isRow = <span style="color: #66cc66;">&#40;</span>ksubstr<span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">upcase</span><span style="color: #66cc66;">&#40;</span>direction<span style="color: #66cc66;">&#41;</span>, <span style="color: #2e8b57; font-weight: bold;">1</span>, <span style="color: #2e8b57; font-weight: bold;">3</span><span style="color: #66cc66;">&#41;</span> = <span style="color: #a020f0;">&quot;ROW&quot;</span><span style="color: #66cc66;">&#41;</span>;
  <span style="color: #0000ff;">if</span> isRow <span style="color: #0000ff;">then</span> 
     <span style="color: #0000ff;">return</span><span style="color: #66cc66;">&#40;</span> T<span style="color: #66cc66;">&#40;</span>SortCols<span style="color: #66cc66;">&#40;</span>M`, descend<span style="color: #66cc66;">&#41;</span><span style="color: #66cc66;">&#41;</span> <span style="color: #66cc66;">&#41;</span>;
  <span style="color: #0000ff;">else</span> 
     <span style="color: #0000ff;">return</span><span style="color: #66cc66;">&#40;</span> SortCols<span style="color: #66cc66;">&#40;</span>M, descend<span style="color: #66cc66;">&#41;</span> <span style="color: #66cc66;">&#41;</span>;
finish;
store module=<span style="color: #66cc66;">&#40;</span>SortCols SortMat<span style="color: #66cc66;">&#41;</span>;
&nbsp;
<span style="color: #006400; font-style: italic;">/* sort matrix by cols and make a heat map to visualize the result */</span>
t = <span style="color: #a020f0;">&quot;Matrix Sorted Ascending by Cols&quot;</span>;
A = SortMat<span style="color: #66cc66;">&#40;</span><span style="color: #a020f0;">&quot;col&quot;</span>, M<span style="color: #66cc66;">&#41;</span>;
ods graphics / width=480px height=240px;
<span style="color: #0000ff;">call</span> heatmapcont<span style="color: #66cc66;">&#40;</span>A<span style="color: #66cc66;">&#41;</span> colorramp=<span style="color: #a020f0;">&quot;ThreeColor&quot;</span> displayoutlines=<span style="color: #2e8b57; font-weight: bold;">0</span> xaxistop=<span style="color: #2e8b57; font-weight: bold;">1</span> <span style="color: #0000ff;">title</span>=t;
<span style="color: #006400; font-style: italic;">/* Note: To sort the columns in descending order, use 
   A = SortMat(&quot;col&quot;, M, 1);
*/</span></pre></td></tr></table></div>




<a href="https://blogs.sas.com/content/iml/files/2026/06/SortRows1.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/SortRows1.png" alt="" width="480" height="240" class="alignnone size-full wp-image-59198" srcset="https://blogs.sas.com/content/iml/files/2026/06/SortRows1.png 480w, https://blogs.sas.com/content/iml/files/2026/06/SortRows1-300x150.png 300w, https://blogs.sas.com/content/iml/files/2026/06/SortRows1-164x82.png 164w" sizes="(max-width: 480px) 100vw, 480px" /></a>

<p>
The heat map visualizes the column-wise sorting. For each column, the values in the sorted matrix are 
stored in increasing order. Thus, the first row contains the smallest value in each sample, the second row contains the 
second smallest value, and so on, until the last row, which contains the largest value in each sample.
Equivalently, each column contains the order statistics for the sample.
</p>
<p>
If you want to analyze, for example, the distribution of the minimum values in the samples, 
you can extract the first row. The following statements extract the first row and create a histogram of the 
sampling distribution of the minimum statistic:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* The smallest values are in the first row of the sorted matrix */</span>
sample_min = A<span style="color: #66cc66;">&#91;</span><span style="color: #2e8b57; font-weight: bold;">1</span>,<span style="color: #66cc66;">&#93;</span>;
<span style="color: #0000ff;">title</span> <span style="color: #a020f0;">&quot;Distribution of the Sample Min for N(0,1) Data&quot;</span>;
<span style="color: #0000ff;">call</span> histogram<span style="color: #66cc66;">&#40;</span>sample_min<span style="color: #66cc66;">&#41;</span>;</pre></td></tr></table></div>





<a href="https://blogs.sas.com/content/iml/files/2026/06/SortRows2.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/SortRows2.png" alt="" width="480" height="240" class="alignnone size-full wp-image-59195" srcset="https://blogs.sas.com/content/iml/files/2026/06/SortRows2.png 480w, https://blogs.sas.com/content/iml/files/2026/06/SortRows2-300x150.png 300w, https://blogs.sas.com/content/iml/files/2026/06/SortRows2-164x82.png 164w" sizes="(max-width: 480px) 100vw, 480px" /></a>

<p>
The histogram shows the distribution of the minimum statistic. This distribution is <a href="https://blogs.sas.com/content/iml/2019/07/22/extreme-value-normal-data.html">one of the extreme-value distributions, known as the Gumbel distribution</a>. 
For a standardized normal sample of this size, it is common that the minimum value is near -2, but values less than -3 are possible.
</p>


<h3>Sorting each row of a matrix</h3>
<p>
In the same way, you can pass "row" as the first argument of the SortMat function to independently sort each row of a matrix:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* Suppose you organize the simulation so that each row is a random sample. Then the
   simulated samples are the rows of M`. You can sort the rows by calling the SortMat(&quot;row&quot;,...) function. */</span>
M = M`;
t = <span style="color: #a020f0;">&quot;Matrix Sorted Ascending by Rows&quot;</span>;
B = SortMat<span style="color: #66cc66;">&#40;</span><span style="color: #a020f0;">&quot;row&quot;</span>, M<span style="color: #66cc66;">&#41;</span>;
ods graphics / width=280px height=480px;
<span style="color: #0000ff;">call</span> heatmapcont<span style="color: #66cc66;">&#40;</span>B<span style="color: #66cc66;">&#41;</span> colorramp=<span style="color: #a020f0;">&quot;ThreeColor&quot;</span> displayoutlines=<span style="color: #2e8b57; font-weight: bold;">0</span> xaxistop=<span style="color: #2e8b57; font-weight: bold;">1</span> <span style="color: #0000ff;">title</span>=t;
<span style="color: #006400; font-style: italic;">/* To sort in descending order, use
B = SortMat(&quot;row&quot;, M, 1);  */</span></pre></td></tr></table></div>




<a href="https://blogs.sas.com/content/iml/files/2026/06/SortRows3-1.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/SortRows3-1.png" alt="" width="280" height="480" class="alignnone size-full wp-image-59237" srcset="https://blogs.sas.com/content/iml/files/2026/06/SortRows3-1.png 280w, https://blogs.sas.com/content/iml/files/2026/06/SortRows3-1-175x300.png 175w" sizes="(max-width: 280px) 100vw, 280px" /></a>


<p>
Instead of generating new data, I simply transposed the previous simulated matrix.
For the transposed matrix, each row is a random sample.
If you sort by rows, the columns contain the distributions of the quantiles.
The i_th column contains the i_th smallest value in each sample. 
</p>

<h3>Summary</h3>
<p>
This article creates a SAS IML function that can independently sort each column or each row of a matrix. 
One application is that you can easily analyze the distribution of order statistics, including minima, maxima,
and other quantiles. 
Another application is to <a href="https://blogs.sas.com/content/iml/2016/06/08/lasagna-plot-in-sas.html">sort the columns of a lasagna plot</a>, 
which can visualize changes to the distribution of a response variable over time.
</p>

<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/06/24/sort-rows-cols-matrix.html">Sort the rows or columns of a matrix independently</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blogs.sas.com/content/iml/2026/06/24/sort-rows-cols-matrix.html/feed</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			<enclosure url="https://blogs.sas.com/content/iml/files/2026/06/SortRows1-150x150.png" />
	</item>
		<item>
		<title>Create two types of Pareto charts in SAS</title>
		<link>https://blogs.sas.com/content/iml/2026/06/22/pareto-charts-sas.html</link>
					<comments>https://blogs.sas.com/content/iml/2026/06/22/pareto-charts-sas.html#comments</comments>
		
		<dc:creator><![CDATA[Rick Wicklin]]></dc:creator>
		<pubDate>Mon, 22 Jun 2026 09:20:27 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Data Analysis]]></category>
		<category><![CDATA[Statistical Graphics]]></category>
		<guid isPermaLink="false">https://blogs.sas.com/content/iml/?p=59116</guid>

					<description><![CDATA[<p>A Pareto chart is a popular chart for statistical quality control. It is often used to display the relative frequencies of issues that affect the quality of a manufacturing process. A bar chart displays the frequency of each issue that causes a defect. The bars are ordered by height: the [...]</p>
<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/06/22/pareto-charts-sas.html">Create two types of Pareto charts in SAS</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>
A Pareto chart is a popular chart for statistical quality control.
It is often used to display the relative frequencies of issues that affect the 
quality of a manufacturing process. A bar chart displays the frequency of each issue that causes a defect. 
The bars are ordered by height:
the most common issues (tall bars) are displayed to the left of the chart, whereas
less common issues (short bars) are displayed to the right. 
In many cases, the prevalence of issues follows a <a href="https://blogs.sas.com/content/iml/2018/04/23/80-20-rule-for-blogs.html">Pareto principle</a>.
The classical Pareto principle is that 80% of the problems are caused by 20% of the issues. 
Therefore, the left side of a Pareto chart reveals which issue (or issues) should be addressed to have the most impact on the quality of
the process.
</p>
<a href="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart1.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart1.png" alt="" width="480" height="360" class="alignright size-full wp-image-59161" srcset="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart1.png 640w, https://blogs.sas.com/content/iml/files/2026/06/ParetoChart1-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a>


<p> 
There are two common variations of the Pareto plot.
The most familiar is a bar chart of frequencies that is overlaid with a cumulative frequency  curve.
This graph is shown to the right.
The other is <a href="https://blogs.sas.com/content/iml/2015/04/27/cascade-chart.html">a "cascade chart" (sometimes called a waterfall chart)</a>
that displays the cumulative frequencies as a staircase of bars. 
</p><p>
These charts are produced automatically by <a href="https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/qcug/qcug_pareto_overview.htm">the PARETO procedure in SAS/QC software</a>.
The PARETO procedure has many options for displaying Pareto charts. 
However, not every SAS user has a license for SAS/QC. This article shows how to create basic
Pareto charts by using Base SAS procedures and the SGPLOT procedure.
</p>

<h3>The data</h3>
<p>
To illustrate a Pareto chart, let's use a modification of data from the documentation of the PARETO procedure.
A random sample of 40 defective parts are examined, and the cause of each defect is recorded, as follows:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* Modification of PROC PARETO example */</span>
<span style="color: #000080; font-weight: bold;">data</span> Failure;
<span style="color: #0000ff;">INFILE</span> DATALINES delimiter=<span style="color: #a020f0;">','</span>;
<span style="color: #0000ff;">length</span> Cause $ <span style="color: #2e8b57; font-weight: bold;">16</span>;
<span style="color: #0000ff;">label</span> Cause = <span style="color: #a020f0;">'Cause of Failure'</span>;
<span style="color: #0000ff;">input</span> Cause @@;
datalines;
Corrosion, Oxide Defect, Contamination, Oxide Defect
Oxide Defect, Oxide Defect, Contamination, Metallization
Oxide Defect, Contamination, Contamination, Oxide Defect
Contamination, Contamination, Contamination, Corrosion
Silicon Defect, Contamination, Contamination, Contamination
Contamination, Contamination, Doping, Oxide Defect
Oxide Defect, Metallization, Contamination, Corrosion
Silicon Defect, Contamination, Corrosion, Corrosion
Metallization, Oxide Defect, Contamination, Contamination
Oxide Defect, Doping, Doping, Contamination
;</pre></td></tr></table></div>




<p>
If you have a license for SAS/QC software, you can create a basic Pareto chart with an overlaid cumulative curve by using the PARETO procedure. So that you can easily reuse the SAS code for your own data, I have defined macro variables for the name of the data set and the name of the categorical variable.
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #0000ff;">%let</span> <span style="color: #0000ff;">DSName</span> = Failure;
<span style="color: #0000ff;">%let</span> <span style="color: #0000ff;">VarName</span> = Cause;
<span style="color: #000080; font-weight: bold;">proc pareto</span> <span style="color: #000080; font-weight: bold;">data</span>=&amp;<span style="color: #0000ff;">DSName</span>;
   vbar &amp;<span style="color: #0000ff;">VarName</span>;
<span style="color: #000080; font-weight: bold;">run</span>;</pre></td></tr></table></div>




<p>
The graph is shown at the top of this article. It shows that "Contamination" is responsible for 42.5% of the failures. 
The first three categories account for 80% of the defects, so those are the issues that quality engineers should strive to improve.
Whereas the classic "Pareto rule" is that 20% of the issues result in 80% of the defects, the cumulative curve 
shows that, for this process, 50% of the issues (the first three categories) are responsible for about 80% of the defects. 
</p>

<h3>A Pareto chart in Base SAS</h3>
<p>
If you do not have a license for SAS/QC software, you can still create a basic Pareto chart by using PROC FREQ to
compute the frequencies and PROC SGPLOT to display a sorted bar chart and a cumulative curve.
There are a few useful options to know about:
</p>
<ol>
<li>The PROC FREQ statement supports the ORDER=FREQ option, which sorts the frequencies in descending order.
</li>
<li>The TABLE statement supports the OUT= option, which you can use to 
create a data set that contains the data for the Pareto chart. Use the OUTCUM option to output the cumulative statistics.
</li>
<li>The SGPLOT procedure supports the VBAR and VLINE statements, which enables you to overlay a bar chart and a cumulative curve.
You can use the Y2AXIS option on the VLINE statement to display a separate axis for the cumulative scale.
</li>
</ol>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* create the Pareto charts by hand in SGPLOT */</span>
<span style="color: #000080; font-weight: bold;">proc freq</span> <span style="color: #000080; font-weight: bold;">data</span>=&amp;<span style="color: #0000ff;">DSName</span> <span style="color: #0000ff;">order</span>=Freq noprint;
   <span style="color: #0000ff;">where</span> <span style="color: #0000ff;">not</span> <span style="color: #0000ff;">missing</span><span style="color: #66cc66;">&#40;</span>&amp;<span style="color: #0000ff;">VarName</span><span style="color: #66cc66;">&#41;</span>;
   <span style="color: #0000ff;">table</span> &amp;<span style="color: #0000ff;">VarName</span> / out=FreqOut outcum;
<span style="color: #000080; font-weight: bold;">run</span>;
&nbsp;
<span style="color: #0000ff;">title</span> <span style="color: #a020f0;">&quot;Standard Pareto Chart&quot;</span>;
<span style="color: #000080; font-weight: bold;">proc sgplot</span> <span style="color: #000080; font-weight: bold;">data</span>=FreqOut noautolegend;
   vbar &amp;<span style="color: #0000ff;">VarName</span> / response=Percent;
   xaxis type=discrete discreteorder=<span style="color: #000080; font-weight: bold;">data</span>;
   yaxis grid <span style="color: #0000ff;">min</span>=<span style="color: #2e8b57; font-weight: bold;">0</span> <span style="color: #0000ff;">max</span>=<span style="color: #2e8b57; font-weight: bold;">100</span> offsetmin=<span style="color: #2e8b57; font-weight: bold;">0</span> <span style="color: #0000ff;">label</span>=<span style="color: #a020f0;">&quot;Percent&quot;</span>;
   <span style="color: #006400; font-style: italic;">/* overlay the cumulative percentage on the Y2 axis */</span>
   vline &amp;<span style="color: #0000ff;">VarName</span> /response=cum_Pct markers datalabel y2axis;
   y2axis <span style="color: #0000ff;">min</span>=<span style="color: #2e8b57; font-weight: bold;">0</span> <span style="color: #0000ff;">max</span>=<span style="color: #2e8b57; font-weight: bold;">100</span> offsetmin=<span style="color: #2e8b57; font-weight: bold;">0</span> <span style="color: #0000ff;">label</span>=<span style="color: #a020f0;">&quot;Cumulative Percent&quot;</span>;
   <span style="color: #0000ff;">format</span> cum_Pct best4.;
<span style="color: #000080; font-weight: bold;">run</span>;</pre></td></tr></table></div>




<a href="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart2.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart2.png" alt="" width="480" height="360" class="alignnone size-full wp-image-59158" srcset="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart2.png 640w, https://blogs.sas.com/content/iml/files/2026/06/ParetoChart2-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a>


<p>
The output is similar to the previous Pareto chart.
I decided to add grid lines and to add markers to the cumulative curve. Otherwise,
the information in this chart is the same as the one produced by PROC PARETO.
</p>

<h3>Problem with the classical Pareto chart</h3>
<p>
The classical Pareto chart suffers from three problems <a href="https://www.tandfonline.com/doi/abs/10.1198/000313006X152243">(Wilkinson, TAS, 2006)</a>:
</p>
<ol>
<li>Dual scale: Mathematically, the cumulative curve is the integral (or sum) of the bar heights.
It can be confusing to plot both quantities in the same graph. The "dual scale" problem is even worse if you show the scale of the bars as counts instead of percentages, as is often done.
</li>
<li>Range: When there are many categories (or when no category is responsible for most of the defects),
the bars are short, whereas the cumulative curve will always range to 100%. 
In that situation, it is difficult to judge the height of the bars, which get squashed down to the bottom of the frame.
</li>
<li>Interpolation: It is wrong to use line segments to connect the cumulative frequencies.
It makes it seem like the cumulative probability is a continuous function that increases linearly between categories.
It is not. For each category, it is a single number. The number increases discontinuously. The
categories are discrete so there is nothing "between" them.  
</li>
<li>
Reference distribution: Implicitly, calling something a "Pareto chart" implies that the data follows a Pareto distribution.
In a Pareto distribution, the frequencies follow a power law. The assumption of an underlying power law 
justifies focusing on the issue that has the largest frequency. If the categories are equally likely
to occur, you might as well focus on the issues that are cheapest or quickest to resolve. 
</li>
</ol>


<h3>A Pareto cascade chart</h3>
<p>
An alternative to the classical Pareto chart addresses 
the first three problems. I call the revised chart <a href="https://blogs.sas.com/content/iml/2015/04/27/cascade-chart.html">a cascade chart</a> or a <em>cumulative Pareto chart</em>.
A bar chart places the base of each bar along a common baseline at zero.
In contrast, a cascade chart aligns the base of each bar with the
top of the preceding bar. This results in a staircase-shaped graph.
The height of each "step" is proportional to the relative frequency of each category.
The tops of the steps show the cumulative distribution of frequencies.
</p>
<p>
If you have access to the PARETO procedure, you can use the CHARTTYPE=CUMULATIVE option
to create a cascade chart, as follows:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #000080; font-weight: bold;">proc pareto</span> <span style="color: #000080; font-weight: bold;">data</span>=&amp;<span style="color: #0000ff;">DSName</span>;
   vbar &amp;<span style="color: #0000ff;">VarName</span> / charttype=cumulative;
<span style="color: #000080; font-weight: bold;">run</span>;</pre></td></tr></table></div>




<a href="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart3.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart3.png" alt="" width="480" height="360" class="alignnone size-full wp-image-59155" srcset="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart3.png 640w, https://blogs.sas.com/content/iml/files/2026/06/ParetoChart3-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a>

<p>
In this graph, the tops of the "steps" show the cumulative distribution of frequencies.
The heights of the steps show each category's contributions.
</p>
<p>
You can also create the chart in Base SAS. First, run the PROC FREQ step that was shown earlier.
This creates the FreqOut data set. The following DATA step uses the LAG function to compute the 
base level for each step.
You can then use a high-low plot to display the staircase-shaped cascade chart:
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* add upper and lower variables for the HIGHLOW plot */</span>
<span style="color: #000080; font-weight: bold;">data</span> CumPareto;
<span style="color: #0000ff;">set</span> FreqOut;
_Lower = <span style="color: #0000ff;">lag</span><span style="color: #66cc66;">&#40;</span>cum_Pct<span style="color: #66cc66;">&#41;</span>;
<span style="color: #0000ff;">if</span> <span style="color: #0000ff;">_N_</span> = <span style="color: #2e8b57; font-weight: bold;">1</span> <span style="color: #0000ff;">then</span> 
   _Lower = <span style="color: #2e8b57; font-weight: bold;">0</span>; 
<span style="color: #000080; font-weight: bold;">run</span>;
&nbsp;
<span style="color: #0000ff;">title</span> <span style="color: #a020f0;">&quot;Cumulative Pareto Chart&quot;</span>;
<span style="color: #000080; font-weight: bold;">proc sgplot</span> <span style="color: #000080; font-weight: bold;">data</span>=CumPareto;
   highlow <span style="color: #0000ff;">x</span>=&amp;<span style="color: #0000ff;">VarName</span> low=_Lower high=cum_Pct / 
           type=bar barwidth=<span style="color: #2e8b57; font-weight: bold;">1</span> highlabel=Percent;
   yaxis offsetmin=<span style="color: #2e8b57; font-weight: bold;">0</span> grid;
   xaxis type=discrete discreteorder=<span style="color: #000080; font-weight: bold;">data</span>;
   <span style="color: #0000ff;">format</span> Percent best4.;
<span style="color: #000080; font-weight: bold;">run</span>;</pre></td></tr></table></div>




<a href="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart4.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart4.png" alt="" width="480" height="360" class="alignnone size-full wp-image-59152" srcset="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart4.png 640w, https://blogs.sas.com/content/iml/files/2026/06/ParetoChart4-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a>

<p>
Again, I have added gridlines and labels to the bars, but otherwise the information is the same as the cumulative chart that is created by PROC PARETO.
This chart has only a single axis, which shows the cumulative probability. Short bars are no longer squashed at the bottom of the chart. By adding labels, you can see the size of the relative frequencies and the cumulative distribution on a common scale.
</p>
<p>
There is a third type of Pareto chart that addresses the problem of not being able to see a reference distribution. I will discuss that variation in a separate article.
</p>

<h3>Summary</h3>
<p>
In SAS, the PARETO procedure in SAS/QC software provides options to create many types of Pareto charts. 
If you do not have a license for SAS/QC software, this article shows how to use Base SAS to create two simple Pareto charts. You can use a Pareto chart to identify the most frequent categories. In practice,
the most frequent categories are analyzed by quality engineers in hopes that addressing those issues will lead to a major improvement in the quality of a manufacturing process.
</p>
<p>
To simplify the creation of basic Pareto charts, I have encapsulated the techniques in this article into two SAS macros, which are shown in the Appendix.
</p>

<h3>Appendix: SAS macros that create basic Pareto charts</h3>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* Macros to create a basic Pareto chart, written by Rick Wicklin. For details, see
   https://blogs.sas.com/content/iml/2026/06/22/pareto-charts-sas.html
*/</span>
<span style="color: #0000ff;">%macro</span> StdPareto<span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">DSName</span>, <span style="color: #0000ff;">VarName</span><span style="color: #66cc66;">&#41;</span>;
<span style="color: #000080; font-weight: bold;">proc freq</span> <span style="color: #000080; font-weight: bold;">data</span>=&amp;<span style="color: #0000ff;">DSName</span> <span style="color: #0000ff;">order</span>=Freq noprint;
   <span style="color: #0000ff;">where</span> <span style="color: #0000ff;">not</span> <span style="color: #0000ff;">missing</span><span style="color: #66cc66;">&#40;</span>&amp;<span style="color: #0000ff;">VarName</span><span style="color: #66cc66;">&#41;</span>;
   <span style="color: #0000ff;">table</span> &amp;<span style="color: #0000ff;">VarName</span> / out=_FreqOut outcum;
<span style="color: #000080; font-weight: bold;">run</span>;
&nbsp;
<span style="color: #000080; font-weight: bold;">proc sgplot</span> <span style="color: #000080; font-weight: bold;">data</span>=_FreqOut noautolegend;
   vbar &amp;<span style="color: #0000ff;">VarName</span> / response=Percent;
   xaxis type=discrete discreteorder=<span style="color: #000080; font-weight: bold;">data</span>;
   yaxis grid <span style="color: #0000ff;">min</span>=<span style="color: #2e8b57; font-weight: bold;">0</span> <span style="color: #0000ff;">max</span>=<span style="color: #2e8b57; font-weight: bold;">100</span> offsetmin=<span style="color: #2e8b57; font-weight: bold;">0</span> <span style="color: #0000ff;">label</span>=<span style="color: #a020f0;">&quot;Percent&quot;</span>;
   <span style="color: #006400; font-style: italic;">/* overlay the cumulative percentage on the Y2 axis */</span>
   vline &amp;<span style="color: #0000ff;">VarName</span> /response=cum_Pct markers datalabel y2axis;
   y2axis <span style="color: #0000ff;">min</span>=<span style="color: #2e8b57; font-weight: bold;">0</span> <span style="color: #0000ff;">max</span>=<span style="color: #2e8b57; font-weight: bold;">100</span> offsetmin=<span style="color: #2e8b57; font-weight: bold;">0</span> <span style="color: #0000ff;">label</span>=<span style="color: #a020f0;">&quot;Cumulative Percent&quot;</span>;
   <span style="color: #0000ff;">format</span> cum_Pct best4.;
<span style="color: #000080; font-weight: bold;">run</span>;
<span style="color: #0000ff;">%mend</span>;
&nbsp;
<span style="color: #0000ff;">%macro</span> CumPareto<span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">DSName</span>, <span style="color: #0000ff;">VarName</span><span style="color: #66cc66;">&#41;</span>;
   <span style="color: #000080; font-weight: bold;">proc freq</span> <span style="color: #000080; font-weight: bold;">data</span>=&amp;<span style="color: #0000ff;">DSName</span> <span style="color: #0000ff;">order</span>=Freq noprint;
      <span style="color: #0000ff;">where</span> <span style="color: #0000ff;">not</span> <span style="color: #0000ff;">missing</span><span style="color: #66cc66;">&#40;</span>&amp;<span style="color: #0000ff;">VarName</span><span style="color: #66cc66;">&#41;</span>;
      <span style="color: #0000ff;">table</span> &amp;<span style="color: #0000ff;">VarName</span> / out=_FreqOut outcum;
   <span style="color: #000080; font-weight: bold;">run</span>;
&nbsp;
   <span style="color: #006400; font-style: italic;">/* add upper and lower variables for the HIGHLOW plot */</span>
   <span style="color: #000080; font-weight: bold;">data</span> _CumPareto;
   <span style="color: #0000ff;">set</span> _FreqOut;
   _Lower = <span style="color: #0000ff;">lag</span><span style="color: #66cc66;">&#40;</span>cum_Pct<span style="color: #66cc66;">&#41;</span>;
   <span style="color: #0000ff;">if</span> <span style="color: #0000ff;">_N_</span> = <span style="color: #2e8b57; font-weight: bold;">1</span> <span style="color: #0000ff;">then</span> 
      _Lower = <span style="color: #2e8b57; font-weight: bold;">0</span>; 
   <span style="color: #000080; font-weight: bold;">run</span>;
&nbsp;
   <span style="color: #000080; font-weight: bold;">proc sgplot</span> <span style="color: #000080; font-weight: bold;">data</span>=_CumPareto;
      highlow <span style="color: #0000ff;">x</span>=&amp;<span style="color: #0000ff;">VarName</span> low=_Lower high=cum_Pct / 
              type=bar barwidth=<span style="color: #2e8b57; font-weight: bold;">1</span> highlabel=Percent;
      yaxis  offsetmin=<span style="color: #2e8b57; font-weight: bold;">0</span> grid;
      xaxis type=discrete discreteorder=<span style="color: #000080; font-weight: bold;">data</span>;
      <span style="color: #0000ff;">format</span> Percent best4.;
   <span style="color: #000080; font-weight: bold;">run</span>;
<span style="color: #0000ff;">%mend</span>;
&nbsp;
<span style="color: #006400; font-style: italic;">/* show how to call each macro. The categorical variable can be 
   numeric or character */</span>
<span style="color: #0000ff;">title</span>;
<span style="color: #006400; font-style: italic;">/* character variable */</span>
%StdPareto<span style="color: #66cc66;">&#40;</span>sashelp.cars, Type<span style="color: #66cc66;">&#41;</span>;
%CumPareto<span style="color: #66cc66;">&#40;</span>sashelp.cars, Type<span style="color: #66cc66;">&#41;</span>;
<span style="color: #006400; font-style: italic;">/* numeric variable */</span>
%StdPareto<span style="color: #66cc66;">&#40;</span>sashelp.cars, Cylinders<span style="color: #66cc66;">&#41;</span>;
%CumPareto<span style="color: #66cc66;">&#40;</span>sashelp.cars, Cylinders<span style="color: #66cc66;">&#41;</span>;</pre></td></tr></table></div>





<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/06/22/pareto-charts-sas.html">Create two types of Pareto charts in SAS</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blogs.sas.com/content/iml/2026/06/22/pareto-charts-sas.html/feed</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			<enclosure url="https://blogs.sas.com/content/iml/files/2026/06/ParetoChart2-150x150.png" />
	</item>
		<item>
		<title>Create a confidence band for a normal Q-Q plot</title>
		<link>https://blogs.sas.com/content/iml/2026/06/15/confidence-band-qqplot.html</link>
					<comments>https://blogs.sas.com/content/iml/2026/06/15/confidence-band-qqplot.html#respond</comments>
		
		<dc:creator><![CDATA[Rick Wicklin]]></dc:creator>
		<pubDate>Mon, 15 Jun 2026 09:23:04 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Data Analysis]]></category>
		<category><![CDATA[Statistical Programming]]></category>
		<guid isPermaLink="false">https://blogs.sas.com/content/iml/?p=59026</guid>

					<description><![CDATA[<p>This article shows how to compute a confidence band for a Q-Q plot in SAS. A previous article shows how to construct confidence bands for the CDF of continuous univariate data. The bands can be added to a plot of the empirical CDF (ECDF) for the data. One of the [...]</p>
<p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/06/15/confidence-band-qqplot.html">Create a confidence band for a normal Q-Q plot</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>
This article shows how to 
compute a confidence band for a Q-Q plot in SAS.
</p>
<p>
A previous article shows <a href="https://blogs.sas.com/content/iml/2026/06/08/confidence-bands-ecdf.html">how to construct confidence bands for the CDF of continuous univariate data</a>.
The bands can be added to a plot of the empirical CDF (ECDF) for the data.
One of the drawbacks of an ECDF plot is that the cumulative distribution is an S-shaped curve. If you display 
two S-shaped curves that represent cumulative distributions, it is difficult for the human eye to detect
differences between them, which is why statisticians plot histograms (estimates of the probability density) more often than 
ECDF curves (estimates of the cumulative distribution).
</p><p>
If you want to compare the data distribution to a theoretical parametric distribution (such as normal or Weibull), 
<a href="https://blogs.sas.com/content/iml/2011/10/28/modeling-the-distribution-of-data-create-a-qq-plot.html">the quantile-quantile (Q-Q) plot</a>
is a useful alternative to the CDF plot.
A Q-Q plot is a visual representation of a statistical goodness-of-fit test: it helps you assess whether the data are a 
random sample from a theoretical distribution. 
Data that are sampled from the specified 
parametric distribution appear to be linear in a Q-Q plot.
</p><p>
More correctly, they appear to be linear for most random samples. 
As discussed in a previous article, <a href="https://blogs.sas.com/content/iml/2016/11/23/sampling-variation-small-samples.html">a Q-Q plot might look nonlinear due to random variation</a>, especially for a small sample. 
Consequently, it can be useful to add a confidence band to the Q-Q plot. 
</p>

<h3>The relationship between the CDF plot and the Q-Q plot</h3>
<p>
It turns out that you can transform the ECDF and confidence bands from the previous article to "straighten out" the cumulative distributions.
The result is a Q-Q plot with a confidence band.
</p>
<p>
Recall that if X is distributed according to some distribution, F, then F(X) is uniformly distributed. 
And if U is uniformly distributed, then F<sup>-1</sup> is distributed according to F.
Details and an example are discussed in <a href="https://blogs.sas.com/content/iml/2021/06/23/probability-integral-transform.html">a previous article on the probability integral transformation</a>.
You can use this result to "straighten out" the ECDF and the confidence bands, provided that you choose a distribution 
to use for the transformation. I will transform the simple Kolmogorov band from the previous article, but you can apply this technique to any other confidence band.
</p>

<a href="https://blogs.sas.com/content/iml/files/2026/06/normalQuantile.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/normalQuantile.png" alt="" width="360" height="270" class="alignright size-full wp-image-59062" srcset="https://blogs.sas.com/content/iml/files/2026/06/normalQuantile.png 640w, https://blogs.sas.com/content/iml/files/2026/06/normalQuantile-300x225.png 300w" sizes="(max-width: 360px) 100vw, 360px" /></a>

<p>
The most familiar distribution is the standard normal distribution. The standard cumulative distribution function is usually denoted as
F = &Phi;. In SAS, you can apply &Phi; by calling the function <code class="preserve-code-formatting">CDF(&quot;Normal&quot;, x)</code>.
You can apply the inverse transformation,  &Phi;<sup>-1</sup>, by calling the SAS function <code class="preserve-code-formatting">QUANTILE(&quot;Normal&quot;, p)</code>.
The inverse normal CDF transformation is shown to the right. Notice that the function is very steep when the probability is near 0 or 1.
This means that a small change in probability results in a very large change in the associated quantiles.
</p>

<h3>Constructing the Q-Q Plot Confidence Bands in SAS IML</h3>
<p>
For data, let's analyze <a href="https://blogs.sas.com/content/iml/2026/05/26/create-ecdf.html">the breaking strength data of fiber-optic cords</a> from previous posts. 
</p>
<p>
A previous article shows <a href="https://blogs.sas.com/content/iml/2011/10/28/modeling-the-distribution-of-data-create-a-qq-plot.html">how to construct a normal Q-Q plot in SAS</a>
by using the following steps:
</p>
<ol>
<li>
Sort the data.
</li>
<li>
Compute n evenly spaced points in the interval (0,1), where n is the number of data points in your sample.
SAS procedures often use Blom's formula, where the i_th point is <em>v<sub>i</sub> = (i - 0.375) / (n + 0.25)</em>. 
</li>
<li>
Compute the quantiles (inverse CDF) of the evenly spaced points. This gives you quantiles of the standard normal distribution.
</li>
<li>
Create a scatter plot of the sorted data versus the standard quantiles computed in Step 3.  It is traditional to put the data
on the vertical axis and the theoretical quantiles on the horizontal axis. This is in contrast to histograms and ECDF plots,
which place the data on the horizontal axis. 
</li>
If you want to add confidence bands to the Q-Q plot, you can transform confidence bands for the CDF.
The transformation maps probabilities in (0,1) into standardized quantiles.
</ol>

<p>
The following SAS IML program uses the <code class="preserve-code-formatting">ECDF</code> and <code class="preserve-code-formatting">ECDF_KSCL</code> modules
from earlier articles. 
For your convenience, you can <a href="https://github.com/sascommunities/the-do-loop-blog/blob/master/ECDF/ECDF_QQ.sas">download these functions from GitHub</a>.
The program compute Kolmogorov bands for the ECDF, transforms them into quantile scale, and writes the results to a data set for graphing. 
</p>


<div class="wp_syntax"><table><tr><td class="code"><pre class="sas" style="font-family:monospace;"><span style="color: #006400; font-style: italic;">/* Before running this program, STORE the ECDF and ECDF_KSCL modules and define the 
   Cord data set. See
   https://blogs.sas.com/content/iml/2026/05/26/create-ecdf.html
   https://blogs.sas.com/content/iml/2026/06/08/confidence-bands-ecdf.html
*/</span>
<span style="color: #000080; font-weight: bold;">proc iml</span>;
<span style="color: #006400; font-style: italic;">/* In SAS Viya, the ECDF function is built into SAS IML and does not need to be loaded. */</span>
load module=<span style="color: #66cc66;">&#40;</span>ECDF ECDF_KSCL<span style="color: #66cc66;">&#41;</span>;  
&nbsp;
<span style="color: #006400; font-style: italic;">/* Read and sort the data */</span>
use Cord;  read all <span style="color: #0000ff;">var</span> <span style="color: #a020f0;">&quot;Strength&quot;</span> <span style="color: #0000ff;">into</span> <span style="color: #0000ff;">x</span>;  <span style="color: #0000ff;">close</span>;
<span style="color: #0000ff;">call</span> sort<span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">x</span><span style="color: #66cc66;">&#41;</span>;
<span style="color: #0000ff;">n</span> = countn<span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">x</span><span style="color: #66cc66;">&#41;</span>;
&nbsp;
<span style="color: #006400; font-style: italic;">/* Compute theoretical plotting positions and map to standardized quantiles (Blom, 1958) */</span>
v = <span style="color: #66cc66;">&#40;</span><span style="color: #66cc66;">&#40;</span><span style="color: #2e8b57; font-weight: bold;">1</span>:<span style="color: #0000ff;">n</span><span style="color: #66cc66;">&#41;</span> - <span style="color: #2e8b57; font-weight: bold;">0.375</span><span style="color: #66cc66;">&#41;</span> / <span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">n</span> + <span style="color: #2e8b57; font-weight: bold;">0.25</span><span style="color: #66cc66;">&#41;</span>;  
q = quantile<span style="color: #66cc66;">&#40;</span><span style="color: #a020f0;">&quot;Normal&quot;</span>, v<span style="color: #66cc66;">&#41;</span>;
&nbsp;
<span style="color: #006400; font-style: italic;">/* Compute the ECDF and the Kolmogorov probability bands */</span>
y = ECDF<span style="color: #66cc66;">&#40;</span><span style="color: #0000ff;">x</span><span style="color: #66cc66;">&#41;</span>;
KS_band = ECDF_KSCL<span style="color: #66cc66;">&#40;</span>y<span style="color: #66cc66;">&#41;</span>;   <span style="color: #006400; font-style: italic;">/* 95% CL */</span>
&nbsp;
<span style="color: #006400; font-style: italic;">/* Optionally, transform probability bands to quantile bounds.
   The domain of the QUANTILE function is the open interval (0,1),
   so clip the band values.
*/</span>
F_Lower = choose<span style="color: #66cc66;">&#40;</span>KS_band<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#93;</span> &gt; <span style="color: #2e8b57; font-weight: bold;">0</span>, KS_band<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">1</span><span style="color: #66cc66;">&#93;</span>, .<span style="color: #66cc66;">&#41;</span>;
F_Upper = choose<span style="color: #66cc66;">&#40;</span>KS_band<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#93;</span> &lt; <span style="color: #2e8b57; font-weight: bold;">1</span>, KS_band<span style="color: #66cc66;">&#91;</span>,<span style="color: #2e8b57; font-weight: bold;">2</span><span style="color: #66cc66;">&#93;</span>, .<span style="color: #66cc66;">&#41;</span>;
<span style="color: #006400; font-style: italic;">/* For a normal Q-Q plot, use the quantile of the normal distribution.
   Use the quantile function for other distributions (e.g., &quot;Exponential&quot;) to 
   construct confidence intervals for other Q-Q plots. */</span>
Q_Lower = quantile<span style="color: #66cc66;">&#40;</span><span style="color: #a020f0;">&quot;Normal&quot;</span>, F_Lower<span style="color: #66cc66;">&#41;</span>;
Q_Upper = quantile<span style="color: #66cc66;">&#40;</span><span style="color: #a020f0;">&quot;Normal&quot;</span>, F_Upper<span style="color: #66cc66;">&#41;</span>;
&nbsp;
<span style="color: #006400; font-style: italic;">/* Create a dataset for plotting */</span>
<span style="color: #0000ff;">create</span> QQ_Bands <span style="color: #0000ff;">var</span> <span style="color: #66cc66;">&#123;</span><span style="color: #a020f0;">&quot;x&quot;</span> <span style="color: #a020f0;">&quot;q&quot;</span> <span style="color: #a020f0;">&quot;Q_Lower&quot;</span> <span style="color: #a020f0;">&quot;Q_Upper&quot;</span><span style="color: #66cc66;">&#125;</span>;
append;
<span style="color: #0000ff;">close</span>;
<span style="color: #000080; font-weight: bold;">QUIT</span>;
&nbsp;
<span style="color: #006400; font-style: italic;">/* Create the Q-Q plot. The data is plotted on the vertical axis.
   The theoretical quantiles are plotted on the horizontal axis. */</span>
<span style="color: #0000ff;">title</span> <span style="color: #a020f0;">&quot;Normal Q-Q Plot with 95% Confidence Bands&quot;</span>;
<span style="color: #000080; font-weight: bold;">proc sgplot</span> <span style="color: #000080; font-weight: bold;">data</span>=QQ_Bands noautolegend;
   <span style="color: #006400; font-style: italic;">/* Draw the confidence bands */</span>
   series <span style="color: #0000ff;">x</span>=Q_Lower y=<span style="color: #0000ff;">x</span> / lineattrs=<span style="color: #66cc66;">&#40;</span>color=gray<span style="color: #66cc66;">&#41;</span>;
   series <span style="color: #0000ff;">x</span>=Q_Upper y=<span style="color: #0000ff;">x</span> / lineattrs=<span style="color: #66cc66;">&#40;</span>color=gray<span style="color: #66cc66;">&#41;</span>;
   <span style="color: #006400; font-style: italic;">/* Overlay a scatter plot of the quantiles of the data vs the standard normal quantiles. */</span>
   scatter <span style="color: #0000ff;">x</span>=q y=<span style="color: #0000ff;">x</span>;
   xaxis <span style="color: #0000ff;">label</span>=<span style="color: #a020f0;">&quot;Theoretical Normal Quantiles&quot;</span> grid;
   yaxis <span style="color: #0000ff;">label</span>=<span style="color: #a020f0;">&quot;Sample Quantiles (Strength)&quot;</span> grid;
<span style="color: #000080; font-weight: bold;">run</span>;</pre></td></tr></table></div>




<a href="https://blogs.sas.com/content/iml/files/2026/06/QQConfidence2.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/QQConfidence2.png" alt="" width="480" height="360" class="alignnone size-full wp-image-59068" srcset="https://blogs.sas.com/content/iml/files/2026/06/QQConfidence2.png 640w, https://blogs.sas.com/content/iml/files/2026/06/QQConfidence2-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a>

<p>
For your convenience, I have also written <a href="https://github.com/sascommunities/the-do-loop-blog/blob/master/ECDF/ECDF_QQ.sas">an IML function (QQ_KSCL) that encapsulates the statements that generate the Q-Q plot and confidence bands</a>. 
</p>

<h3>Interpreting the Q-Q Plot Bands</h3>

<p>
Intuitively, 
the Q-Q plot and its confidence bands are a visual depiction of a hypothesis test.
The Q-Q plot indicates whether the data might be a random sample from a normal distribution.
If the scatter plot is approximately linear, then the assumption of normality seems reasonable.
</p><p>

But what does "approximately linear" mean? By how much can a Q-Q plot deviate from linearity without rejecting the assumption of normality?
The confidence bands indicate that the scatter plot can bend a little bit in the center
of the distribution and can bend more severely in the tails. 
It is said that George Box used to place a fat pencil on a Q-Q plot. 
If he could adjust the pencil so that it covered all points,
then he assumed that the data were approximately normal. The confidence band 
provides a similar visual indicator. If you can draw a straight line 
that sits entirely within the confidence bands, the data might be approximately normal. 
You can run a more formal analysis to test that hypothesis.
</p>
<p>
The widening of the confidence bands in the extreme quantiles explains why points in the tails of a Q-Q plot often deviate from the linear reference line or exhibit a slight bend. Without confidence bands, you might mistakenly conclude that these deviations are evidence of skewness, heavy tails, or non-normality. The confidence bands indicate that deviations in the tail are expected due to natural sampling variability. If the scatter points are inside the bands, there is insufficient evidence to reject the null hypothesis that the data comes from a normal distribution.
</p>

<h3>A normal Q-Q plot for non-normal data</h3>
<p>
Before concluding, let's take a quick look at a similar graph for data that are not normal. I generated a random sample of 50 data from an exponential distribution and ran the same SAS code. The Q-Q plot for the exponential data looks like the following graph:
</p>

<a href="https://blogs.sas.com/content/iml/files/2026/06/QQConfidence1.png"><img loading="lazy" decoding="async" src="https://blogs.sas.com/content/iml/files/2026/06/QQConfidence1.png" alt="" width="480" height="360" class="alignnone size-full wp-image-59059" srcset="https://blogs.sas.com/content/iml/files/2026/06/QQConfidence1.png 640w, https://blogs.sas.com/content/iml/files/2026/06/QQConfidence1-300x225.png 300w" sizes="(max-width: 480px) 100vw, 480px" /></a>

<p>
If you apply Box's "fat pencil" test, it is clear that you cannot cover the scatter points with a pencil. 
You should conclude that the normal distribution is not a good fit for these data. 
</p>
<p>
The Kolmogorov bands in this plot are so wide that you 
might actually be able to draw a line that stays inside these confidence bands. However, the scatter points are not near that line.
A weakness of the Kolmogorov-Smirnov test for normality (and the Kolmogorov confidence bands) is low statistical power for small samples.
The Kolmogorov bounds themselves do not rule out the possibility of fitting a normal distribution to these data, but the nonlinear shape of the data points suggest that the fit is not good.
</p>

<h3>Summary</h3>

<p>
The standard normal quantile function is a transformation that maps probabilities onto quantiles. 
By transforming the non-parametric Kolmogorov bands from an ECDF, you can generate simultaneous confidence bands for a normal Q-Q plot. 
You could use a different quantile function to obtain Q-Q plots for other distributions.
</p>
<p>
For moderately sized samples, if you can draw a straight line that stays inside the bands, you might want to perform a formal 
test for the hypothesis that the data are normally distributed. The Kolmogorov bands have low power, so be careful using them for small samples.
</p>
<p>
You can <a href="https://github.com/sascommunities/the-do-loop-blog/blob/master/ECDF/ECDF_QQ.sas">download all functions from the ECDF and Q-Q plot articles</a>. You can also <a href="https://github.com/sascommunities/the-do-loop-blog/blob/master/ECDF/ECDF_QQ_examples.sas">download a program that creates all tables and graphs in this series of blog posts</a>.</p><p>The post <a rel="nofollow" href="https://blogs.sas.com/content/iml/2026/06/15/confidence-band-qqplot.html">Create a confidence band for a normal Q-Q plot</a> appeared first on <a rel="nofollow" href="https://blogs.sas.com/content/iml">The DO Loop</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blogs.sas.com/content/iml/2026/06/15/confidence-band-qqplot.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<enclosure url="https://blogs.sas.com/content/iml/files/2026/06/QQConfidence2-150x150.png" />
	</item>
	</channel>
</rss>
