<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://fjyang.me/feed.xml" rel="self" type="application/atom+xml" /><link href="https://fjyang.me/" rel="alternate" type="text/html" /><updated>2026-08-20T16:30:41-07:00</updated><id>https://fjyang.me/feed.xml</id><title type="html">Fengjun Yang</title><subtitle>Notes on control, robotics, and the systems around them.</subtitle><author><name>Fengjun Yang</name></author><entry><title type="html">NumPy Fundamentals for Technical Interviews</title><link href="https://fjyang.me/writing/numpy-fundamentals-for-technical-interviews/" rel="alternate" type="text/html" title="NumPy Fundamentals for Technical Interviews" /><published>2026-08-20T00:00:00-07:00</published><updated>2026-08-20T00:00:00-07:00</updated><id>https://fjyang.me/writing/numpy-fundamentals-for-technical-interviews</id><content type="html" xml:base="https://fjyang.me/writing/numpy-fundamentals-for-technical-interviews/"><![CDATA[<p>These are the NumPy mechanisms and patterns I wanted at my fingertips while preparing for technical interviews. The goal is not to catalog the entire API, but to build enough intuition to reason about shapes, memory, and performance without trial and error.</p>

<h2 id="arrays-memory-and-strides">Arrays, memory, and strides</h2>

<p>A multidimensional NumPy array consists of two main pieces:</p>

<ol>
  <li>a one-dimensional block of data; and</li>
  <li>metadata describing how that data should be interpreted.</li>
</ol>

<p>The most important metadata for understanding layout is the array’s <strong>strides</strong>. A stride records how many bytes NumPy moves in memory when an index advances by one along a given axis.</p>

<p>For example, a C-contiguous array with shape <code class="language-plaintext highlighter-rouge">(3, 4)</code> and dtype <code class="language-plaintext highlighter-rouge">int64</code> has strides <code class="language-plaintext highlighter-rouge">(32, 8)</code>:</p>

<ul>
  <li>advancing one row jumps (4 \times 8 = 32) bytes;</li>
  <li>advancing one column jumps (8) bytes.</li>
</ul>

<p>Slicing and transposing often change only this metadata. NumPy can point a new array object at the same memory while assigning it a different shape, offset, or set of strides.<sup id="fnref:views" role="doc-noteref"><a href="#fn:views" class="footnote" rel="footnote">1</a></sup></p>

<h2 id="why-vectorization-is-fast">Why vectorization is fast</h2>

<p>Vectorized NumPy code usually benefits from three things:</p>

<ul>
  <li><strong>Cache efficiency.</strong> Contiguous data makes good use of the adjacent memory fetched into CPU caches.</li>
  <li><strong>Less Python overhead.</strong> Operations and loops run in compiled code over typed data instead of repeatedly dispatching dynamically typed Python objects.</li>
  <li><strong>SIMD instructions.</strong> NumPy’s compiled backend can use vector registers to apply one instruction to several values at once.</li>
</ul>

<p>Vectorization is not magic, however. A concise expression can still allocate an enormous intermediate array.</p>

<h2 id="broadcasting">Broadcasting</h2>

<p>NumPy compares operand shapes from right to left. Two dimensions are compatible when they are equal or when either one is <code class="language-plaintext highlighter-rouge">1</code>. Missing dimensions on the left are treated as dimensions of size <code class="language-plaintext highlighter-rouge">1</code>.</p>

<p>Some representative shape calculations are:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(A, B, C) + (A, B, C) -&gt; (A, B, C)
(A, 1, C) + (1, B, C) -&gt; (A, B, C)
(A, B, C) +    (B, 1) -&gt; (A, B, C)
</code></pre></div></div>

<p>I collected the exercises I used to practice these patterns in my <a href="https://github.com/FJYang96/interview/tree/main/vectorization">vectorization interview-practice repository</a>.</p>

<h3 id="pairwise-squared-distances">Pairwise squared distances</h3>

<p>Given <code class="language-plaintext highlighter-rouge">X</code> with shape <code class="language-plaintext highlighter-rouge">(B, D)</code> and <code class="language-plaintext highlighter-rouge">Y</code> with shape <code class="language-plaintext highlighter-rouge">(C, D)</code>, form every pairwise difference and reduce along the feature dimension:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dist_sq</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">((</span><span class="n">X</span><span class="p">[:,</span> <span class="bp">None</span><span class="p">,</span> <span class="p">:]</span> <span class="o">-</span> <span class="n">Y</span><span class="p">[</span><span class="bp">None</span><span class="p">,</span> <span class="p">:,</span> <span class="p">:])</span> <span class="o">**</span> <span class="mi">2</span><span class="p">,</span> <span class="n">axis</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
<span class="c1"># shape: (B, C)
</span></code></pre></div></div>

<p>The inserted axes turn the operands into <code class="language-plaintext highlighter-rouge">(B, 1, D)</code> and <code class="language-plaintext highlighter-rouge">(1, C, D)</code>, which broadcast to <code class="language-plaintext highlighter-rouge">(B, C, D)</code>.<sup id="fnref:pairwise" role="doc-noteref"><a href="#fn:pairwise" class="footnote" rel="footnote">2</a></sup></p>

<h3 id="batched-matrix-vector-products">Batched matrix-vector products</h3>

<p>Given <code class="language-plaintext highlighter-rouge">X</code> with shape <code class="language-plaintext highlighter-rouge">(B, M, N)</code> and <code class="language-plaintext highlighter-rouge">y</code> with shape <code class="language-plaintext highlighter-rouge">(B, N)</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">result</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">X</span> <span class="o">*</span> <span class="n">y</span><span class="p">[:,</span> <span class="bp">None</span><span class="p">,</span> <span class="p">:],</span> <span class="n">axis</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
<span class="c1"># shape: (B, M)
</span></code></pre></div></div>

<h3 id="constructing-a-mask-from-lengths">Constructing a mask from lengths</h3>

<p>Given sequence lengths <code class="language-plaintext highlighter-rouge">lengths</code> with shape <code class="language-plaintext highlighter-rouge">(B,)</code>, construct a mask with shape <code class="language-plaintext highlighter-rouge">(B, S)</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mask</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="n">S</span><span class="p">)[</span><span class="bp">None</span><span class="p">,</span> <span class="p">:]</span> <span class="o">&gt;=</span> <span class="n">lengths</span><span class="p">[:,</span> <span class="bp">None</span><span class="p">]</span>
</code></pre></div></div>

<p>Here <code class="language-plaintext highlighter-rouge">mask[b, s]</code> is true when position <code class="language-plaintext highlighter-rouge">s</code> lies at or beyond the valid length of item <code class="language-plaintext highlighter-rouge">b</code>. Use <code class="language-plaintext highlighter-rouge">&gt;</code> instead of <code class="language-plaintext highlighter-rouge">&gt;=</code> if the boundary convention includes <code class="language-plaintext highlighter-rouge">lengths[b]</code> itself.</p>

<h2 id="indexing-and-masking">Indexing and masking</h2>

<p>Suppose <code class="language-plaintext highlighter-rouge">arr</code> has shape <code class="language-plaintext highlighter-rouge">(B, M, N)</code>. Basic slicing usually returns a view, while advanced indexing with boolean or integer arrays returns a copy because the selected elements need not be adjacent in memory.</p>

<h3 id="boolean-masks">Boolean masks</h3>

<p>A full mask has the same shape as the array:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">selected</span> <span class="o">=</span> <span class="n">arr</span><span class="p">[</span><span class="n">mask</span><span class="p">]</span>  <span class="c1"># mask: (B, M, N), result: (K,)
</span></code></pre></div></div>

<p>The selected values are flattened into a one-dimensional result.</p>

<p>A prefix mask can select complete feature vectors:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">selected</span> <span class="o">=</span> <span class="n">arr</span><span class="p">[</span><span class="n">mask</span><span class="p">]</span>  <span class="c1"># mask: (B, M), result: (K, N)
</span></code></pre></div></div>

<p>A one-dimensional mask can filter one axis:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">arr</span><span class="p">[</span><span class="n">batch_mask</span><span class="p">,</span> <span class="p">:,</span> <span class="p">:]</span>  <span class="c1"># batch_mask: (B,), result: (K, M, N)
</span><span class="n">arr</span><span class="p">[:,</span> <span class="n">row_mask</span><span class="p">,</span> <span class="p">:]</span>    <span class="c1"># row_mask: (M,), result: (B, K, N)
</span></code></pre></div></div>

<h3 id="integer-array-indexing">Integer-array indexing</h3>

<p>Multiple index arrays select coordinates together. The index arrays must be broadcastable to a common shape.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># idx1 and idx2 both have shape (K,)
</span><span class="n">out</span> <span class="o">=</span> <span class="n">arr</span><span class="p">[</span><span class="n">idx1</span><span class="p">,</span> <span class="n">idx2</span><span class="p">,</span> <span class="p">:]</span>
<span class="c1"># out[k, n] == arr[idx1[k], idx2[k], n]
# shape: (K, N)
</span></code></pre></div></div>

<p>Broadcasting the indices creates a grid of selections:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># idx1: (P, 1), idx2: (1, Q)
</span><span class="n">out</span> <span class="o">=</span> <span class="n">arr</span><span class="p">[</span><span class="n">idx1</span><span class="p">,</span> <span class="n">idx2</span><span class="p">,</span> <span class="p">:]</span>
<span class="c1"># out[p, q, n] == arr[idx1[p, 0], idx2[0, q], n]
# shape: (P, Q, N)
</span></code></pre></div></div>

<p>A single integer index array replaces the indexed axis with the index array’s shape:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">arr</span><span class="p">[</span><span class="n">idx</span><span class="p">].</span><span class="n">shape</span>  <span class="c1"># idx: (K,)    -&gt; (K, M, N)
</span><span class="n">arr</span><span class="p">[</span><span class="n">idx</span><span class="p">].</span><span class="n">shape</span>  <span class="c1"># idx: (K, J) -&gt; (K, J, M, N)
</span></code></pre></div></div>

<h2 id="shape-manipulation">Shape manipulation</h2>

<h3 id="reshape-and-singleton-dimensions">Reshape and singleton dimensions</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">reshape</code> returns a view when the requested layout is compatible with the underlying memory; otherwise it may return a copy.</li>
  <li><code class="language-plaintext highlighter-rouge">np.expand_dims(a, axis)</code> inserts one or more singleton dimensions.</li>
  <li><code class="language-plaintext highlighter-rouge">np.squeeze(a, axis=None)</code> removes singleton dimensions.</li>
  <li><code class="language-plaintext highlighter-rouge">keepdims=True</code> preserves reduced axes with size <code class="language-plaintext highlighter-rouge">1</code>, which often makes later broadcasting easier.</li>
</ul>

<h3 id="permuting-axes">Permuting axes</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">np.transpose(a, axes)</code> specifies the complete axis order. Without <code class="language-plaintext highlighter-rouge">axes</code>, it reverses the axes.</li>
  <li><code class="language-plaintext highlighter-rouge">np.swapaxes(a, axis1, axis2)</code> exchanges two axes.</li>
</ul>

<p>The terminology differs slightly in PyTorch:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>torch.transpose  &lt;-&gt; np.swapaxes
torch.permute    &lt;-&gt; np.transpose
</code></pre></div></div>

<h3 id="combining-arrays">Combining arrays</h3>

<p><code class="language-plaintext highlighter-rouge">concatenate</code> joins arrays along an existing axis, so every other dimension must match:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">np</span><span class="p">.</span><span class="n">concatenate</span><span class="p">((</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">),</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">stack</code> inserts a new axis, so all input shapes must match:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">np</span><span class="p">.</span><span class="n">stack</span><span class="p">((</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">),</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="reductions">Reductions</h2>

<p>The reductions I use most often are:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="n">axis</span><span class="p">)</span>
<span class="n">np</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="n">axis</span><span class="p">)</span>
<span class="n">np</span><span class="p">.</span><span class="nb">max</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="n">axis</span><span class="p">)</span>
<span class="n">np</span><span class="p">.</span><span class="n">argmax</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="n">axis</span><span class="p">)</span>
<span class="n">np</span><span class="p">.</span><span class="nb">any</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="n">axis</span><span class="p">)</span>
<span class="n">np</span><span class="p">.</span><span class="nb">all</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="n">axis</span><span class="p">)</span>
<span class="n">np</span><span class="p">.</span><span class="n">cumsum</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="n">axis</span><span class="p">)</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">axis</code> argument specifies which dimension disappears. With <code class="language-plaintext highlighter-rouge">keepdims=True</code>, that dimension remains with length <code class="language-plaintext highlighter-rouge">1</code>.</p>

<h2 id="einsum">Einsum</h2>

<p><code class="language-plaintext highlighter-rouge">np.einsum</code> describes tensor operations by naming axes. In an expression such as</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"str1,str2-&gt;str3"
</code></pre></div></div>

<p>the output axes are exactly those on the right-hand side. An axis that appears on the left but not the right is summed over. An empty right-hand side produces a scalar.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Batched matrix multiplication
</span><span class="n">np</span><span class="p">.</span><span class="n">einsum</span><span class="p">(</span><span class="s">"bmk,bkn-&gt;bmn"</span><span class="p">,</span> <span class="n">A</span><span class="p">,</span> <span class="n">B</span><span class="p">)</span>

<span class="c1"># A @ B.T
</span><span class="n">np</span><span class="p">.</span><span class="n">einsum</span><span class="p">(</span><span class="s">"mn,pn-&gt;mp"</span><span class="p">,</span> <span class="n">A</span><span class="p">,</span> <span class="n">B</span><span class="p">)</span>

<span class="c1"># trace(A)
</span><span class="n">np</span><span class="p">.</span><span class="n">einsum</span><span class="p">(</span><span class="s">"ii-&gt;"</span><span class="p">,</span> <span class="n">A</span><span class="p">)</span>

<span class="c1"># x.T @ P @ y
</span><span class="n">np</span><span class="p">.</span><span class="n">einsum</span><span class="p">(</span><span class="s">"i,ij,j-&gt;"</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">P</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>

<span class="c1"># Pairwise query-key scores for self-attention
</span><span class="n">np</span><span class="p">.</span><span class="n">einsum</span><span class="p">(</span><span class="s">"bid,bjd-&gt;bij"</span><span class="p">,</span> <span class="n">Q</span><span class="p">,</span> <span class="n">K</span><span class="p">)</span>
</code></pre></div></div>

<p>For an interview, I find it useful to write down each operand’s shape, label every axis, and then identify which labels survive in the output.</p>

<h2 id="performance-intuition">Performance intuition</h2>

<p>Vectorization becomes problematic when it creates large temporary arrays. Pairwise operations are a common example: broadcasting may avoid Python loops while still requiring (O(BCD)) memory for an intermediate result.</p>

<p><code class="language-plaintext highlighter-rouge">np.where(cond, a, b)</code> can also be wasteful when computing <code class="language-plaintext highlighter-rouge">a</code> and <code class="language-plaintext highlighter-rouge">b</code> is expensive. Both branches are generally evaluated before <code class="language-plaintext highlighter-rouge">where</code> selects between them, so masked assignment or indexing may be preferable when only a small subset is needed.</p>

<h2 id="useful-operations">Useful operations</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Preserve reduced dimensions for later broadcasting
</span><span class="n">total</span> <span class="o">=</span> <span class="n">x</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">axis</span><span class="o">=-</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdims</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="c1"># Accumulate weights into integer-indexed bins
</span><span class="n">totals</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">bincount</span><span class="p">(</span><span class="n">bin_indices</span><span class="p">,</span> <span class="n">weights</span><span class="o">=</span><span class="n">weights</span><span class="p">)</span>

<span class="c1"># Elementwise conditional selection
</span><span class="n">result</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">where</span><span class="p">(</span><span class="n">condition</span><span class="p">,</span> <span class="n">value_if_true</span><span class="p">,</span> <span class="n">value_if_false</span><span class="p">)</span>
</code></pre></div></div>

<p>The recurring theme is simple: track shapes explicitly, know when an operation creates a view or a copy, and estimate the size of broadcasted intermediates before relying on vectorization.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:views" role="doc-endnote">
      <p>This is why changing a slice can sometimes modify its source array. Use <code class="language-plaintext highlighter-rouge">.copy()</code> when the result must own independent data. <a href="#fnref:views" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:pairwise" role="doc-endnote">
      <p>This expression materializes an array of shape <code class="language-plaintext highlighter-rouge">(B, C, D)</code>. For very large inputs, a blocked algorithm or a specialized distance routine can use much less memory. <a href="#fnref:pairwise" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Fengjun Yang</name></author><category term="numpy" /><category term="python" /><category term="interviews" /><summary type="html"><![CDATA[A compact review of array memory, broadcasting, indexing, shape manipulation, einsum, and performance pitfalls.]]></summary></entry></feed>