<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Feiza Brothers on Medium]]></title>
        <description><![CDATA[Stories by Feiza Brothers on Medium]]></description>
        <link>https://medium.com/@blakefeiza?source=rss-5da05b3c7452------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*EmUvJOULicwvPm-HzCok1A.png</url>
            <title>Stories by Feiza Brothers on Medium</title>
            <link>https://medium.com/@blakefeiza?source=rss-5da05b3c7452------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 16 May 2026 16:35:54 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@blakefeiza/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Rethinking A/B Testing with Bayesian Inference]]></title>
            <link>https://medium.com/@blakefeiza/rethinking-a-b-testing-with-bayesian-inference-6f9dca94ed2f?source=rss-5da05b3c7452------2</link>
            <guid isPermaLink="false">https://medium.com/p/6f9dca94ed2f</guid>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[bayesian-statistics]]></category>
            <category><![CDATA[statistics]]></category>
            <dc:creator><![CDATA[Feiza Brothers]]></dc:creator>
            <pubDate>Wed, 13 May 2026 05:01:00 GMT</pubDate>
            <atom:updated>2026-05-13T05:01:00.487Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*X2x_jDDdyV8kGZKxD8KkYQ.png" /><figcaption>Thumbnail</figcaption></figure><p>Running an A/B test sounds straightforward. Split your test data, measure an outcome, and pick a winner. Yet assumptions in the most common A/B tests can break down in ways that are easy to miss and costly to ignore. Luckily, there are alternative approaches that treat belief as something to be updated as evidence arrives, rather than a mere threshold to be crossed. In this post, we’ll build our knowledge on the Bayesian statistical framework and examine what it really costs to get decisions wrong.</p><h3><strong>The Issue with Frequentist Probability</strong></h3><p>The <strong>Frequentist </strong>definition of probability is the most widely known in statistics. It states that probability measures the long-run frequency of an event occurring over many repeated trials. A classic example is the coin flip. Flipping a coin millions of times, the proportion of heads gradually stabilizes near 0.5 as an estimate of the true underlying probability. That true probability is always unknown, but with enough repeated trials, we can get reasonably close.</p><p><strong>Frequentist A/B Testing</strong> is an experiment we run to see if two processes behave significantly differently from one another. Consider a pharmaceutical company testing a new treatment to relieve headaches. Patients are randomly assigned to receive treatment A (an existing medication) or treatment B (the new medication). After exposure to the treatment, each patient reports whether their headache was relieved within one hour. The goal of this test is to determine whether treatment B produces a meaningfully higher relief rate than treatment A. The null hypothesis for this A/B test is: Treatments A and B produce identical relief rates. We’d then define a fixed number of patients, each receiving exactly one of the two treatments. Then, we’d use the test results to compute a single p-value answering the question: What’s the probability of observing data as extreme as ours if the null hypothesis is true? If this probability is very unlikely (&lt; 0.05), we’d reject the null hypothesis and claim that the two treatments produce meaningfully different relief rates, resulting in a winner.</p><p>Key takeaways on the Frequentist A/B approach:</p><ul><li>The p-value from this test is the probability of observing our collected data given the <strong>null hypothesis is true</strong>.</li><li>Frequentist probability treats the true relief rates of treatments A and B as <strong>fixed, unknown constants</strong> (probabilities that do not change).</li><li>The false positive rate (0.05) is only controlled <strong>if </strong>we commit to our sample size in advance and look at the results exactly once.</li></ul><p>Let’s say we decide a reasonable sample size for this experiment to be 500 patients. As we carry out this experiment, stakeholders behind the scenes are eager to monitor the results in real time. After the first 50 sessions, the resulting p-value dips to 0.03, indicating we should reject the null hypothesis and favor treatment B. The issue is, every time we glance at the accumulating results <strong>before </strong>reaching our defined sample size of 500, we risk rejecting the null hypothesis by chance alone. This is known as <strong>p-hacking by peeking</strong>, which can actually inflate the 0.05 false positive rate we thought we were controlling. Research suggests that peeking at live results just ten times can balloon a 0.05 false positive rate to upwards of 0.25 <a href="https://www.evanmiller.org/how-not-to-run-an-ab-test.html">(Miller, 2010)</a>. So why peek at all? Interim results can be tempting to those who have incentive to act on promising data early, even when the data isn’t fully conclusive. Frequentist A/B testing is meant for a world where we resist that pressure, but that’s not always the case in practice.</p><p>After 100 sessions, treatment B has a relief rate of 68% and treatment A has a relief rate of 61%. Is treatment B truly better yet? If we’re playing by the frequentist rules, we’re in a tough spot. We’ve been monitoring the number of collected sessions, but we still haven’t reached our pre-defined sample size of 500 (and frankly, we’re questioning whether 100 independent patient sessions could have been a sufficient sample size to begin with). At this point, the p-value still cannot tell us: <strong>Given the data, how confident should we be that treatment B is genuinely better?</strong> Lucky for us, Bayesian inference can help us answer this question.</p><p><strong>What is Bayesian Statistics?</strong></p><p>The <strong>Bayesian </strong>definition of probability is a measurement of belief. It represents a value of uncertainty about an event, whether it happens once or many times. Our belief in the outcome of an event can change as we observe new evidence. Instead of committing to a single fixed estimate, we maintain a full distribution of beliefs across all possible parameter values, updating these beliefs as new evidence arrives.</p><p>Let’s revisit the coin flip for a moment. Under frequentist thinking, we flip a coin millions of times, and the probability of landing on heads will converge to a fixed value around 0.5. With Bayesian probability, we start with a belief about the coin’s bias, like “I expect the true probability of landing on heads to fall between 0.4 and 0.6.” We might suspect the coin is fair, but we’re not certain. Well, we can revise that belief with every flip. After 10 flips, our distribution is wide and uncertain. After 10,000 flips, it has narrowed considerably. While both Frequentist and Bayesian estimates in this example might lead to the same result, the Bayesian approach allows us to carry a full picture of our estimates and uncertainty along the way.</p><p>We’ll use this same approach now for the clinical drug experiment. Instead of asking whether the relief rate of treatment B is significantly different than treatment A at some fixed sample size, we’ll construct a <strong>distribution of beliefs</strong> around both treatments’ true relief rates, updating both in real time as patient sessions are recorded. To understand how that updating works, we need Bayes’ Theorem. Let’s recall our favorite <strong>conditional probability</strong> formula:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/203/1*ZdRaIz5w1H012yf_UQlysg.png" /></figure><p>In our experiment, we’re not talking about abstract events A and B. We care about parameters and data. Let’s rewrite this formula as:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/203/1*qSZKuWGlcCl-dBCMiQjD8Q.png" /></figure><p>Where <strong>θ </strong>is the unknown parameter we want to learn (the true relief rate for a particular treatment) and <strong>X </strong>is the observed data (the recorded outcomes of relieved and not relieved from real patient sessions).</p><p>Next, we need to define a few new terms:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Aw193YoUsVcCbrizQl0GNQ.png" /></figure><p>The posterior is always what we’re after. It reflects our updated belief about θ given everything observed so far.</p><p>Let’s put this knowledge to the test with a famous example in Bayesian probability.</p><p><em>Consider a disease that affects 1% of the population. A diagnostic test for this disease is 95% accurate: if you have the disease, the test returns positive 95% of the time. If you don’t have the disease, the test returns negative 95% of the time (a 5% false positive rate). You test positive. What is the probability you actually have the disease?</em></p><p>Here’s how we go about answering this question using the formulas above.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/776/1*OQDVEOyLPDdh1zDYghNbEw.png" /></figure><p>Drumroll…</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/392/1*wmEapWzd5-uHQdNR5f9YwQ.png" /></figure><p>There’s actually only a 16% chance you have the disease despite a 95% accurate test. Many people (my initial self included) assume the answer is closer to 95%. The disease is so rare that false positives outnumber true positives across the full population. Our prior belief (only 1% of people have this disease) is powerful enough to override a test that is 95% accurate. Thus, the posterior is a balance between prior belief and observed evidence. If you were to test positive twice in a row, that posterior of 16% becomes your new prior going into the next update. Each result reshapes your belief, which becomes the starting point for the next posterior.</p><p><strong><em>A side note on the likelihood P(X)</em></strong></p><p>Notice P(X) sits in the denominator of the posterior formula above. Its only job is to ensure the posterior sums to 1 across all possible values of θ. It has no dependence on θ and is a constant regardless of the parameter value we’re evaluating. Since we’ll be focused on identifying the <strong>shape of the posterior distribution</strong>, a constant that scales everything uniformly doesn’t meaningfully change anything. We can drop the denominator and write:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/210/1*0f2CljtCF_KXunm-104WmA.png" /><figcaption>Where ∝ means “<em>proportional to</em>”</figcaption></figure><p>Again, the shape of the posterior is determined <strong>entirely by the numerator</strong>, so we’ll refer to this formula from here on out.</p><p>Recall Frequentist probability views the true relief rate of treatment B as a fixed, unknown constant. We can only <strong>estimate </strong>this true relief rate by collecting data, running a test, and arriving at a single value like 68%. We can add a confidence interval around our estimate, but we’re ultimately left with a single estimate and a range. There’s no formal way for us to say we believe the true rate is probably around 68% with a reasonable chance it’s as high as 75%. We can’t update that estimate fluidly as new sessions arrive either. The Bayesian play is to represent θ (the true relief rate) as a <strong>probability distribution</strong>. Instead of committing to a single estimate, we capture a full distribution of our uncertainty across every possible value between 0 and 1. The mean of the distribution is where we think the true θ value most likely falls. The width of the distribution reflects how certain or uncertain we are. Whenever new data arrives and we update our prior, the entire distribution updates.</p><p>A <strong>Beta Distribution</strong> Beta(α, β<strong>)</strong> is a common probability distribution in Bayesian statistics defined on the interval [0,1]. The distribution<strong> </strong>uses two parameters, α (alpha) and β (beta)<strong>, </strong>to represent pseudo-counts of successes and failures measuring θ. We can use this distribution to model our prior belief of θ. For example, think of α as the number of relieved outcomes (successes) and β as the number of non-relieved outcomes (failures) before the clinical drug experiment begins. A Beta(1, 1) prior represents starting as if we’ve seen exactly one success and one failure (essentially no prior bias). A Beta(10, 30) represents having already observed 10 successes from 40 sessions, suggesting a prior belief of roughly 25% relief rate with moderate confidence.</p><p>If we have no prior beliefs on the true relief rates of treatments A or B at the start of the A/B test, we can use the neutral Beta(1,1) as the prior for both treatments. This is a flat, uninformative prior telling the model we have no strong belief about the effectiveness of either treatment before the experiment. After 500 sessions are collected for each treatment, each success and failure to relieve a headache would update the corresponding Beta posterior distributions (one for θ_A , one for θ_B). We’d then ask what the probability is that a sample drawn from B’s posterior exceeds a sample drawn from A’s posterior.</p><p>The probability density function for the Beta distribution is:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/219/1*uB9i9yMdbGUyxEf980NUBQ.png" /></figure><p>Where B(α, β) is the normalizing constant ensuring the distribution integrates to 1 over [0,1]. This is analogous to P(X) in Bayes’ theorem. When we plot the Beta distribution, θ sits on the x-axis. We’re never claiming we know θ. Instead, we’re asking how likely each possible value of θ is given our current beliefs. We’ll take a look at a few of these plots below. Try thinking of them as pictures of uncertainty around the true value of θ.</p><p>Here are some examples of a prior Beta distribution for the relief rate of treatment B. The peak location is always α/(α+β). The width tells us how much volatility is behind that belief.</p><p><strong>Beta(1, 1) — An uninformative prior</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/953/1*TWk6eo_1c9ZaYUi4bN1URg.png" /></figure><p>This is a completely flat distribution. Every possible relief rate from 0 to 1 is equally likely. We’re essentially expressing no prior belief about what to expect.</p><p><strong>Beta(13, 7) — A weak prior belief</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/935/1*OfS76pn3kfoug4f-C_6XBA.png" /></figure><p>Equivalent to observing 13 relieved outcomes from 20 sessions. Soft belief that treatment B’s relief rate is around 65%.</p><p><strong>Beta(204, 96) — A strong prior belief</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/970/1*EuTTUnn3R5BDBPXggp_2Kw.png" /></figure><p>Equivalent to 204 relieved outcomes from 300 sessions. High confidence that treatment B’s relief rate is near 68%.</p><p>Now, here’s where the math gets exciting. If we pair the right prior P(θ) distribution with the right likelihood P(X∣θ) distribution, the posterior comes out as the same family of distributions as the prior. This is called C<strong>onjugacy</strong>, and it’s what makes our update rule clean enough to derive by hand. In the clinical drug experiment, our likelihood comes from a binomial distribution (relieved or not relieved being the binary outcome), and the prior comes from a Beta distribution (defined on the closed [0,1] interval for probability). Together, their combination forms a Beta-Binomial conjugate pair, which can be used to update the prior.</p><p>Let’s break down how this works. Consider the likelihood P(X|θ). If we observe n patient sessions for a particular treatment with k successful relief outcomes, the likelihood of this data given a particular value of θ is:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/226/1*p0CmxIl8BPiPvcsJVYUQsw.png" /></figure><p>This is the Binomial distribution. It answers the question of how probable it is to observe exactly k relief outcomes from n sessions, given a true relief rate of θ. Notice that (n choose k), the binomial coefficient, is only a constant with no dependence on θ. Just like P(X) in Bayes’ theorem, we can drop it when reasoning about the shape of the posterior. So, the likelihood simplifies to:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/188/1*pb7fZnOX8ADNkBPz5srZfQ.png" /></figure><p>To determine the Posterior P(θ|X), we must multiply the likelihood above with our Beta prior. Recall this prior takes the form:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/173/1*_R0cFuiY7v7PCcb1c-3IvQ.png" /></figure><p>Multiplying the likelihood by the prior:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/315/1*GYUutUOYPIKYqMaW-7j3vA.png" /></figure><p>Collecting exponents:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/257/1*HUgWMt5QbuHQ7jDywSdKVw.png" /></figure><p>Upon simplifying, this looks exactly like our Beta distribution, just with updated parameters:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/274/1*SUG2Qna0sgM4jX8utH1Q2Q.png" /></figure><p>That’s the entire update rule! For a specific treatment in the experiment, our prior was B(α, β). We observe k relieved outcomes from n sessions for that treatment. So, our final posterior is B(α+k, β+n-k).</p><p>Here’s an example of how our belief in the true relief rate of treatment B can change as we accumulate data and update our prior with the latest posterior.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*34wHQ-syiMYB9AdURDUq3w.png" /></figure><p>Notice that the mean of the posterior after each data collection converges to the true relief rate as we accumulate more and more patient sessions. Each row’s posterior becomes the prior for the next update. Notice also that the first prior’s influence pulling the mean towards 0.5 fades away quickly as real data accumulates. By session 500, the data has taken over to shape the mean. In the clinical drug experiment, we can run this update simultaneously for both treatments, maintaining Beta(α_A, β_A) and Beta(α_B, β_B) in parallel. Every session that arrives updates one of the two distributions depending on which treatment was prescribed. We are never forced to wait for a pre-defined sample size, and the posteriors simply reflect whatever evidence we’ve seen so far.</p><h3><strong>Making Decisions Under Uncertainty</strong></h3><p>At any point in the clinical drug trial, we’ll have two posterior distributions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/234/1*t8kj00l1ryx5NMW4DvG-EA.png" /></figure><p>Either distribution represents our full belief about that treatment’s true relief rate at that point in the experiment. However, these distributions aren’t enough to guide decisions yet. When a stakeholder asks if treatment B is truly better than treatment A, we must answer two questions first.</p><ol><li>Given the latest data, how likely is it that treatment B is truly better than A?</li><li>What would a wrong decision in the experiment cost us?</li></ol><p>To answer question 1…</p><p>A<strong> Frequentist 95% confidence interval</strong> built around an estimated relief rate won’t tell us there’s a 95% probability the true rate lives inside it. It means that across many repeated experiments, 95% of the constructed intervals would contain the true rate. The true relief rate is always fixed in this lens. A specific confidence interval has it or it doesn’t, and frequentist statistics doesn’t allow us to assign a probability to that.</p><p>A <strong>Bayesian 95% credible interval</strong> is exactly what it sounds like. Given the data observed, there is a 95% probability that the true relief rate lies within this range. In the clinical drug trial, by the time treatment B’s posterior is Beta(341,161), we can hand a stakeholder a credible interval and say with confidence exactly where the true relief rate most likely falls.</p><p>With two independent Beta posterior distributions, we want to know how often a sample drawn from B’s posterior would exceed a sample drawn from A’s posterior. This can be denoted:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/89/1*GPVW3CaBlaDSoURU05O6cw.png" /></figure><p>Analytically, this is computed by integrating over all possible values of θ_A​ and θ_B​:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/321/1*uzQmq_NhGeSdK2dZLFuXiA.png" /></figure><p>The double integral may look intimidating, but the intuition is straightforward. If treatment B’s posterior is almost entirely to the right of treatment A’s on the relief rate axis, P(B&gt;A) approaches 1. If the two distributions heavily overlap, P(B&gt;A) hovers near 0.5, meaning the data hasn’t separated them yet. P(B&gt;A) is useful, but it is not quite the right number to make a final decision on.</p><p>To answer question 2…</p><p>P(B&gt;A) = 0.95 might sound compelling, but what if treatment B is only 0.1% better than treatment A in the scenarios where it wins and 5% worse in the scenarios where it loses? Calling treatment B better might still be the wrong call. This is where expected loss comes in. What is the average cost of making the wrong decision?</p><p>We can define a<strong> loss function</strong> for each possible decision (choosing treatment A or B as superior). In either case, the loss measures how much effectiveness we leave on the table by promoting the inferior treatment. If we promote treatment B and it turns out to be worse, the loss is how much A exceeded B. If we promote treatment A and it turns out to be better, the loss evaluates to zero.</p><p>If we were to promote treatment B:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/276/1*ewkYft2e1DZYDcH_GnAbUQ.png" /></figure><p>Similarly, if we were to promote treatment A:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/282/1*rTE3ufayNFhmlLoKRcUrjQ.png" /></figure><p>We would only decide to promote Treatment B if the expected loss falls below some acceptable threshold ε:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/142/1*37D0gUhUdLSw5wbRUD_D-A.png" /></figure><p><em>For simplicity, we use a shared threshold ε since treatment A is already the incumbent treatment. We could technically introduce a separate ε for promoting an alternative depending on the experiment.</em></p><p>Remember that <strong>Frequentist </strong>A/B testing should never allow early stopping in an experiment. The moment we break our commitment to the sample size, our false positive rate inflates and the results become unreliable. Under a <strong>Bayesian </strong>A/B test, we can stop as soon as the expected loss drops below ε. Whether it takes 100 or 1,000 sessions in the clinical drug experiment, the data will tell us once we’ve collected enough of it.</p><h3>Tying It All Together</h3><p>Let’s close with one last scenario. Imagine you’re a data scientist working for a video streaming platform. The platform recommends new content to viewers as soon as they finish watching a video. You’ve built a new recommendation model and want to know if it drives more clicks than the existing one.</p><p>Model A is the existing model. It recommends content based on what similar viewers have watched. Model B is your new model. It factors in how long the viewer watched their last video for and whether they’ve rewatched content before. Both models show a ranked list of recommendations at the end of every video. The metric is simple enough. Did the viewer click on at least one recommended video?</p><p>We can carry out a simulation exposing 500 viewer sessions to each model. The key unknown in this experiment is the click-through rate (CTR) generated by each model’s recommendation. For our purposes, we’ll set the ground truth CTR for Models A and B at 25% and 28% respectively. This is a modest lift, but reasonable in practice. A 3% difference is meaningful at scale, but subtle enough to go undetected in the early stages of an experiment. That’s what will make this an interesting test case.</p><p>Let’s fire up our Python IDE to get started. We’ll first need to simulate viewer session outcomes from the ground truth Binomial distributions of each model.</p><pre># Import libraries<br>import numpy as np<br>from scipy import stats<br>import matplotlib.pyplot as plt<br>from scipy.special import betaln<br>from scipy.stats import chi2_contingency<br><br># Set seed<br>np.random.seed(100)<br><br># Pre-defined click through rates for simulation (unknown during experiments)<br>true_ctr_a = 0.25<br>true_ctr_b = 0.28<br><br># Number of viewer sessions exposed to each model<br>n_sessions = 500<br><br># Simulate binary outcomes for each model<br>clicks_a = np.random.binomial(1, true_ctr_a, n_sessions)<br>clicks_b = np.random.binomial(1, true_ctr_b, n_sessions)<br><br># Print results<br>print(f&quot;Model A: {clicks_a.sum()} clicks from {n_sessions} sessions &quot;<br>      f&quot;(observed CTR: {clicks_a.mean():.3f})&quot;)<br>      <br>print(f&quot;Model B: {clicks_b.sum()} clicks from {n_sessions} sessions &quot;<br>      f&quot;(observed CTR: {clicks_b.mean():.3f})&quot;)</pre><p>Model A: 124 clicks from 500 sessions (observed CTR: 0.248)</p><p>Model B: 132 clicks from 500 sessions (observed CTR: 0.264)</p><p>We’ll now define a<em> </em>few functions to call later in the script. These pull from everything we’ve covered about Bayesian inference so far.</p><pre># Create a function to update priors from observed data<br>def update_posterior(alpha, beta, click):<br>    &quot;&quot;&quot;<br>    Update a Beta posterior given a single binary outcome.<br>    click = 1 (clicked) increments alpha (successes)<br>    click = 0 (no click) increments beta (failures)<br>    &quot;&quot;&quot;<br>    alpha += click<br>    beta += 1 - click<br>    return alpha, beta<br><br># Create a function to calculate the mean of a posterior distribution<br>def posterior_mean(alpha, beta):<br>    return alpha / (alpha + beta)<br><br># Create a function to calculate the variance of a posterior distribution<br>def posterior_variance(alpha, beta):<br>    return (alpha * beta) / ((alpha + beta) ** 2 * (alpha + beta + 1))<br><br># Create a function to return a credible interval containing 95% of the posterior distribution&#39;s belief about theta<br>def credible_interval(alpha, beta, credibility=0.95):<br>    lower = (1 - credibility) / 2<br>    upper = 1 - lower<br>    return stats.beta.ppf([lower, upper], alpha, beta)<br><br># Create a function to sample n theta values from a Beta posterior distribution<br>def sample_posterior(alpha, beta, n=100_000):<br>    return np.random.beta(alpha, beta, n)<br><br># Create a function to compute P(theta_B &gt; theta_A) for two Beta distributions<br>def prob_b_beats_a(alpha_a, beta_a, alpha_b, beta_b):<br>    total = 0.0<br>    &quot;&quot;&quot;<br>    Sum over each possible success count in B&#39;s posterior<br>    Given i successes for B, how much of A&#39;s distribution falls below that probability?<br>    betaln computes log Beta function values to keep numbers stable at large alpha/betas<br>    &quot;&quot;&quot;<br>    for i in range(alpha_b):<br>        total += np.exp(<br>            betaln(alpha_a + i, beta_a + beta_b) -<br>            np.log(beta_b + i) -<br>            betaln(1 + i, beta_b) -<br>            betaln(alpha_a, beta_a)<br>        )<br>    return total</pre><p>Unlike the last P(B &gt; A) function above, the <strong>expected loss integral</strong> doesn’t have a clean closed-form solution. We’ll use Monte Carlo sampling to draw a large number of θ (CTR) values from both posteriors, comparing them element-wise to simulate the joint distribution of (θ_A, θ_B) and estimate expected loss directly. The law of large numbers guarantees this estimate converges to the true expectation quickly. 100,000 samples will be plenty!</p><pre>def expected_loss(alpha_a, beta_a, alpha_b, beta_b, n_samples=100_000):<br>    &quot;&quot;&quot;<br>    Estimate expected loss for promoting B and promoting A<br>    via Monte Carlo sampling over the joint posterior.<br>    &quot;&quot;&quot;<br>    samples_a = sample_posterior(alpha_a, beta_a, n_samples)<br>    samples_b = sample_posterior(alpha_b, beta_b, n_samples)<br><br>    # Cost of promoting B: how much does A exceed B when A is better?<br>    loss_promote_b = np.mean(np.maximum(samples_a - samples_b, 0))<br><br>    # Cost of promoting A: how much does B exceed A when B is better?<br>    loss_promote_a = np.mean(np.maximum(samples_b - samples_a, 0))<br><br>    return loss_promote_b, loss_promote_a</pre><p>The result of this expected loss function is only useful when paired with a threshold. Recall from earlier that we define an acceptable threshold ε and only promote Model B if the expected loss of shipping Model B is less than ε. We’ll set ε equal to 0.5%, meaning we’re willing to accept a 0.5% hit to our clickthrough rate if we accidentally promote the wrong model.</p><pre># Maximum acceptable CTR loss<br>epsilon = 0.005</pre><p>Assuming we have no prior beliefs about the effectiveness of Model B, we’ll initialize its prior distribution as Beta(1,1). For Model A, its prior performance in production will help construct our prior belief. Let’s represent this belief in Model A’s CTR with a Beta(1,3) distribution.</p><pre># Initialize Beta(1,3) prior for Model A (estimating a 25% CTR)<br>alpha_a, beta_a = 1, 3<br><br># Initialize Beta(1,1) prior for Model B (flat, unbiased)<br>alpha_b, beta_b = 1, 1</pre><p>Next, let’s create a dictionary that logs the evolution of each posterior distribution as data accumulates.</p><pre># Track experiment history session by session<br>history = {<br>    &quot;sessions&quot;: [],<br>    &quot;mean_a&quot;: [],<br>    &quot;mean_b&quot;: [],<br>    &quot;prob_b_beats_a&quot;: [],<br>    &quot;loss_promote_b&quot;: [],<br>    &quot;loss_promote_a&quot;: [],<br>    &quot;decision_made&quot;: None<br>}<br><br># Record initial priors before any data arrives<br>history[&quot;sessions&quot;].append(0)<br>history[&quot;mean_a&quot;].append(posterior_mean(alpha_a, beta_a))<br>history[&quot;mean_b&quot;].append(posterior_mean(alpha_b, beta_b))<br>history[&quot;prob_b_beats_a&quot;].append(prob_b_beats_a(alpha_a, beta_a, alpha_b, beta_b))<br>history[&quot;loss_promote_b&quot;].append(0.0)<br>history[&quot;loss_promote_a&quot;].append(0.0)</pre><p><em>As you and I know at this point, Model B is the superior model, so we’ve shaped some of our functions and simulation results around capturing when we’ve confidently observed this outcome.</em></p><p>Let’s run our first simulation to see when Model B proves victorious in the A/B test!</p><pre>for t in range(1, n_sessions + 1):<br><br>    # Update each posterior with the current session outcome<br>    alpha_a, beta_a = update_posterior(alpha_a, beta_a, clicks_a[t-1])<br>    alpha_b, beta_b = update_posterior(alpha_b, beta_b, clicks_b[t-1])<br><br>    # Compute P(B &gt; A) and expected loss at this point in the experiment<br>    p_b_wins = prob_b_beats_a(alpha_a, beta_a, alpha_b, beta_b)<br>    loss_b, loss_a = expected_loss(alpha_a, beta_a, alpha_b, beta_b)<br><br>    # Record metrics for the current session<br>    history[&quot;sessions&quot;].append(t)<br>    history[&quot;mean_a&quot;].append(posterior_mean(alpha_a, beta_a))<br>    history[&quot;mean_b&quot;].append(posterior_mean(alpha_b, beta_b))<br>    history[&quot;prob_b_beats_a&quot;].append(p_b_wins)<br>    history[&quot;loss_promote_b&quot;].append(loss_b)<br>    history[&quot;loss_promote_a&quot;].append(loss_a)<br><br>    # Promote Model B when expected loss clears epsilon<br>    if history[&quot;decision_made&quot;] is None and loss_b &lt; epsilon:<br>        history[&quot;decision_made&quot;] = t<br>        print(f&quot;Decision at session {t}: Promote Model B&quot;)<br>        print(f&quot;P(B &gt; A):          {p_b_wins:.4f}&quot;)<br>        print(f&quot;Expected loss:     {loss_b:.5f}&quot;)<br>        print(f&quot;Posterior mean A:  {posterior_mean(alpha_a, beta_a):.4f}&quot;)<br>        print(f&quot;Posterior mean B:  {posterior_mean(alpha_b, beta_b):.4f}&quot;)</pre><p>Decision at session <strong>169</strong>: Promote Model B</p><p>P(B &gt; A): <strong>0.8267</strong></p><p>Expected loss: <strong>0.00429</strong></p><p>Posterior mean A: <strong>0.2254</strong></p><p>Posterior mean B: <strong>0.2690</strong></p><p>By session <strong>169, </strong>the expected loss drops below ε, triggering an early decision in favor of Model B. As you can see above, the posterior means didn’t converge to their true CTR values yet. We’re only stating the scenarios sampled from both posteriors produced an acceptably low cost of being wrong about promoting Model B. The Bayesian framework didn’t wait for certainty around the CTR estimates. Instead, the test stopped when the risk of a wrong decision became tolerable.</p><p>To put the Bayesian approach in context, let’s run the same simulation as a Frequentist A/B test. We’ll use a Chi-square test to evaluate results at exactly 169 sessions, with a fixed sample size of 500 viewer sessions per model. Stopping here (peeking early) will highlight just how differently the two frameworks interpret the same underlying data.</p><pre># Run frequentist chi-square test on data up to the Bayesian decision point<br>t_decision = history[&quot;decision_made&quot;]<br><br># Build a contingency table to run the chi-square test on<br>contingency = np.array([<br>    [clicks_a[:t_decision].sum(), t_decision - clicks_a[:t_decision].sum()],<br>    [clicks_b[:t_decision].sum(), t_decision - clicks_b[:t_decision].sum()]<br>])<br><br># Results<br>chi2, p_value, _, _ = chi2_contingency(contingency)<br>print(f&quot;Frequentist test at session {t_decision} (Peeking Early):&quot;)<br>print(f&quot;Chi-square statistic: {chi2:.4f}&quot;)<br>print(f&quot;p-value:              {p_value:.4f}&quot;)<br>print(f&quot;Significant at 0.05:  {p_value &lt; 0.05}&quot;)</pre><p>Frequentist test at session <strong>169 </strong>(Peeking Early):</p><p>Chi-square statistic: <strong>0.5749</strong></p><p>p-value: <strong>0.4483</strong></p><p>Significant at 0.05: <strong>False</strong></p><p>By session 169 in the Frequentist test, a p-value of 0.45 is nowhere near a 0.05 threshold. The test can’t reject the null hypothesis at this point. It demands more data before drawing any meaningful conclusion.</p><p>The Bayesian framework reached a decision at that same session without violating a single assumption. It’s worth noting again: P(B &gt; A) never crossed 0.95 across the entire experiment. The decision wasn’t driven by confidence in the direction of the result. It came down to the expected cost of promoting the wrong model falling below what we were willing to accept. Here’s how that played out across all 500 sessions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JXhDSHUZaQh1n1hqH_1fgA.png" /></figure><p>Plot 1 shows both posterior means converging toward the true CTR values over time. Increasing the number of viewer sessions from 500 to 1,000 would get us even closer to 0.25 and 0.28.</p><p>In plot 2, P(B &gt; A) spends a long stretch below 0.5 early on. The data briefly favored Model A before enough evidence accumulated to separate the two models. You’ll notice it never crosses 0.95, highlighting why expected loss is the better decision criterion.</p><p>Plot 3 tells the clearest story. Expected loss starts high and noisy, then steadily falls as the posteriors sharpen. It crosses epsilon at session 169 and lands us at our answer to promote Model B.</p><p>Bayesian statistics gives us a principled way to make decisions under uncertainty without the constraints of frequentist testing. We maintain a full distribution of beliefs about unknown parameters and update them continuously as new evidence arrives. This lets us answer questions that actually matter in practice. The expected loss framework takes it one step further, replacing an arbitrary significance threshold with a concrete measure of what being wrong actually costs. Next time you’re tempted to peek at that p-value, maybe just let the posterior do the talking.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6f9dca94ed2f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Best Way to Dissect a Tableau Workbook]]></title>
            <link>https://medium.com/@blakefeiza/the-best-way-to-dissect-a-tableau-workbook-1388f9ae42c9?source=rss-5da05b3c7452------2</link>
            <guid isPermaLink="false">https://medium.com/p/1388f9ae42c9</guid>
            <category><![CDATA[tableau]]></category>
            <category><![CDATA[data-lineage]]></category>
            <category><![CDATA[datafam]]></category>
            <category><![CDATA[tableau-desktop]]></category>
            <dc:creator><![CDATA[Feiza Brothers]]></dc:creator>
            <pubDate>Sun, 01 Mar 2026 06:01:00 GMT</pubDate>
            <atom:updated>2026-03-01T06:01:00.746Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*32c60CL0oa4uGqQnHsd51g.png" /></figure><p>Have you ever inherited a workbook that needs updating and found yourself swimming in hundreds of calculated fields with no clear direction? Or maybe it’s your own old workbook that you’re revisiting, and you have absolutely no interest in dissecting it because it has grown into a monster.</p><p>Fortunately, about a year ago, Ana Rey de Castro built a solution for exactly this problem with her Tableau Calculation Extractor. The code is available for free on GitHub and runs locally on your machine, so there is no risk to your files. If you want a walkthrough of the tool, you can also check out a video from Tableau Tim in October 2024 where he discusses the release.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FfgJjpCZ5Rxc%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DfgJjpCZ5Rxc&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FfgJjpCZ5Rxc%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/aeff2a57da367c82082a3a36cc1a09a1/href">https://medium.com/media/aeff2a57da367c82082a3a36cc1a09a1/href</a></iframe><p>I first heard about this tool directly from Ana and used it shortly after to work through a few messy .twbx files. It was incredibly helpful at the time.</p><p>Recently, I ran into another use case for the tool, but this time I was working on a Mac instead of a Windows machine. One limitation of Ana’s original version was that a few of the required packages were Windows-specific, which made the code unusable on macOS.</p><p>With the help of Claude Code, I was able to port Ana’s project over to Mac and successfully run the tool locally on my machine. I’m excited to walk through that process and share it with you today.</p><h3>Where Can I Find the Code?</h3><p>You can find both Ana’s Windows version and the new Mac version here:</p><p><strong>Windows:</strong> <a href="https://github.com/scinana/tableauCalculationExport">https://github.com/scinana/tableauCalculationExport</a></p><p><strong>Mac:</strong> <a href="https://github.com/blakefeiza/Tableau-Calculation-Extractor-Mac/tree/main">https://github.com/blakefeiza/Tableau-Calculation-Extractor-Mac</a></p><h3>How Does it Work?</h3><p>The first step is to download the code to your local machine and open the project directory in your IDE of choice. For this walkthrough, I’ll be using Visual Studio Code, but any editor will work just fine.</p><blockquote>Since I’m on a Mac, this writeup reflects a macOS setup. If you’re working on Windows, I recommend checking out the Tableau Tim video mentioned above, along with Ana’s original walkthrough.</blockquote><p>After downloading the repository, create two folders in the root directory: one called inputs and one called outputs. The inputs folder is where you’ll place the .twbx file you want to analyze. The outputs folder is where the generated PDF, XLSX, or HTML files will appear after running the notebook.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zvqYeSjHi0vjLQwKDBBtZQ.png" /></figure><p>For this demo, I’ll be using my <a href="https://public.tableau.com/app/profile/blake.feiza/viz/365DaysofPlay/RadialSportsCalendar"><strong>365 Days of Play</strong></a> workbook. I downloaded the packaged workbook from Tableau Public and dropped the .twbx file into the inputs folder we just created.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7Gz4XvBrFzEuPC5aa1RLGA.png" /></figure><p>Now we’re ready to open the notebook.</p><p>In the top cell, review the required packages and confirm that everything is installed on your machine. I left the most common dependencies listed in that first cell for convenience. If anything is missing, go ahead and install it before running the rest of the notebook.</p><p>Once the dependencies are handled, the notebook should run in just a few seconds. After it completes, check your outputs folder. You should see the generated artifacts waiting for you.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6rvLoHCv3qAcaEFv_qbSDQ.png" /></figure><p>Both the PDF and Excel outputs provide a comprehensive table listing all field names, field types, calculation logic, and associated data sources for the entire workbook.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VR8_uVLaYPAYNT0T95ox4g.png" /></figure><p>One difference you may notice on the Mac version is that instead of the static Mermaid diagram used in the Windows version, this implementation renders a dynamic Cytoscape diagram. The result is an interactive view of your calculation lineage.</p><p>I found this especially helpful when working with larger workbooks. You can drill into individual calculations, isolate the lineage for a specific field, and quickly collapse or expand sections as needed. You can also drag and reposition the nodes to structure the diagram in whatever way makes the most sense to you.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*wnxi4asZjVQUT7I_fUm-5Q.gif" /></figure><p>This was a shorter post, but I hope some of you find value in this community-built tool. If it makes your life a little easier the next time you need to retrace steps in a complex work project (or a Workout Wednesday challenge), then it has done its job.</p><p>This is more of a proof of concept than a fully polished application. If you have ideas on how to improve it, I’d love to hear them. And once again, a huge thank you to Ana Rey de Castro for building the original version and sharing it with the Tableau Community!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1388f9ae42c9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Stop Overthinking Trellis Charts: Two Calculations]]></title>
            <link>https://medium.com/@blakefeiza/stop-overthinking-trellis-charts-two-calculations-4233b4dca912?source=rss-5da05b3c7452------2</link>
            <guid isPermaLink="false">https://medium.com/p/4233b4dca912</guid>
            <category><![CDATA[datafam]]></category>
            <category><![CDATA[tableau]]></category>
            <category><![CDATA[table-calculation]]></category>
            <category><![CDATA[charts]]></category>
            <category><![CDATA[trellis]]></category>
            <dc:creator><![CDATA[Feiza Brothers]]></dc:creator>
            <pubDate>Mon, 02 Feb 2026 01:36:23 GMT</pubDate>
            <atom:updated>2026-02-02T01:36:23.395Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6uXU7O_Yx3hCHCBzbpMqPA.png" /><figcaption>Thumbnail.</figcaption></figure><p>If you’ve spent some time scrolling on Tableau Public or observed enough data visualizations across the broader internet, odds are you’ve run into a <strong>Trellis Chart</strong>. Trellis Charts are useful because they segment a visual into small, repeated views. These views often share the same scales, and make patterns or differences easy to spot. They let viewers compare categories side-by-side without requiring much brain power.</p><h3>Where Does the Name “Trellis” Come From?</h3><p>Trellis Charts (also sometimes referred to as small multiples, lattice or panel charts) arrange the views in a grid-like layout. When built tall, these charts resemble a garden trellis.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/1*-NrNRzhVKJgttIcQk2Vp8w.png" /><figcaption>Photo from <a href="https://www.wayfair.com/sca/ideas-and-advice/guides/what-is-a-trellis-T5945">Wayfair</a>, displaying a garden trellis.</figcaption></figure><h3>Building a Trellis Chart</h3><p>For this blog post, we will visualize the trend of wins per season for every NFL team. The data used for this analysis comes from the GitHub repo <a href="https://github.com/nflverse/nflreadpy">nflreadpy</a>. Credit to my brother Ethan Feiza for curating the dataset used in this analysis. You can find a copy of the final dataset <a href="https://data.world/blakefeiza/feizadata-february-2025-nfl-wins-coaches">here</a>.</p><p>While we could certainly plot all of these teams on a single-axis line chart and use a highlight action/color palette to select one or many teams to compare, that approach requires either a pre-baked insight or an end user’s appetite to explore the visualization.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/843/1*Pp6Igo1ayOs-QkkiSWJoFA@2x.png" /><figcaption>Grey bowl of spaghetti slapped on a time series axis.</figcaption></figure><p>If we use a Trellis Chart instead, we can enable the user to make their own comparisons much easier with a static image.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9MpgoQl4e7rT1RRVsA38qw@2x.png" /><figcaption>Final Trellis Chart from this blog post.</figcaption></figure><p>In order to create this chart, there are <strong>only two primary calculations</strong> to split up the view, one called <strong>“Rows”</strong> and one called <strong>“Cols”</strong>. After we walk through the initial two calculations, I’ll cover a few of the map layers tricks I chose to add on this viz but note that they are optional additions.</p><p>I’ll start by adding our <strong>[Season] to Columns</strong>, <strong>[Wins] to Rows</strong>, <strong>[Team] to Detail</strong>, and change the <strong>mark type to Area</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*B7yWKKDidcjHe3qZesNT6Q@2x.png" /><figcaption>Default Tableau view after adding a few pills to the area chart.</figcaption></figure><p>Now in order to turn this mess into a trellis chart, we need to add two calculated fields that are each tied to a parameter I like to call <strong>[pCols]</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/553/1*EoI-Gvb3goLPF4wP3tRT_A@2x.png" /><figcaption>Parameter Creation Config Menu.</figcaption></figure><p>Once you’ve created this parameter, create the following two calculated fields:</p><pre>// Cols<br>(INDEX()-1)%[pCols]<br><br>// Rows<br>INT((INDEX()-1)/[pCols])</pre><p>Using some modular arithmetic and rounding, these calculations and our parameter give us the ability to specify how many columns we’d like in our trellis chart and the rows will update dynamically.</p><p>Add <strong>[Cols] to the Columns</strong> and <strong>[Rows] to the Rows</strong>. You should see something like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EiQCsVGCbJHvRdqnFD1oWQ@2x.png" /><figcaption>Default Tableau view after adding [Rows] and [Cols] to their shelves.</figcaption></figure><p>The next thing we need to do is <strong>configure our table calculations</strong> for [Cols] and [Rows]. It doesn’t matter which one we start with because <strong>the steps are identical for both</strong>.</p><p>Right click the blue pill and select “Edit Table Calculation”. Select “Specific Dimensions”, make sure <strong>both your [Season] and [Team] are checked</strong>. <strong>Drag the [Team] field to the top</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/341/1*1bz-nc3Ga0uiNyzoWgIcDQ@2x.png" /><figcaption>Configuring Table Calculation for [Rows] and [Cols] (no map layers)</figcaption></figure><p>By default, our sort order will be alphabetical. If we wanted to do something like order the teams by total wins, we can configure that in the “Sort order” option as follows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/517/1*jETWMevPVRrly0qS36APZA@2x.png" /><figcaption>Updating the sort logic for [Cols] and [Rows] in the “Edit Table Calculation” box.</figcaption></figure><p>Follow these steps for both [Cols] and [Rows] and you should see something like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rT_gi2rauokFa7qX3CQiTQ@2x.png" /><figcaption>Tableau view after editing table calculations (no map layers).</figcaption></figure><p>From here, we can <strong>uncheck “Show Header”</strong> for our [Rows] and [Cols] fields (and optionally clean up [Season] and [Wins]), add some color or additional formatting clean-up, and get something like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/986/1*15hWctVuITd4UZrTmpSzuA@2x.png" /><figcaption>Final chart option without map layers.</figcaption></figure><p>There are some traditional approaches to add labels and additional formatting to our chart from here, but I’ll leave that as an exercise to the reader. Instead, I’d like to dive into the “Map Layers alternate solution” instead to do as much custom formatting as our hearts desire :)</p><h3>Optional: Using Map Layers with Trellis Chart Calculations</h3><p>Okay, I know I said we’d only use two calculations, but this is the bonus section. There are a few more calculations that we’ll need to make in order to turn the above chart into a grid of maps. If you’d like to learn more about these calculations, the in-line comments will prove useful.</p><pre>// Normalized Season<br>// Using the traditional &quot;(Number - Min) / (Max - Min) normalization<br>// This gives us a value between 0 and 1 for every season.<br>// For a map, we need to stay between (-180,180). Staying between 0 and 1 makes the math easier later on.<br>([Season] - {MIN([Season])})<br>/<br>({MAX([Season])} - {MIN([Season])})<br><br>// Wins per Season Makepoint<br>// This plots a point on our map at <br>// Remember MAKEPOINT() uses (Latitude, Longitude) -&gt; backwards from (X,Y)<br>// I like to think of it as MAKEPOINT(Rise, Run)<br>MAKEPOINT(SUM([Wins]), MIN([Normalized Season]))</pre><p>On a new sheet, <strong>double click [Wins per Season Makepoint]</strong>, change your <strong>Mark Type to Line</strong>, drag <strong>[Team] to Detail</strong>, and <strong>[Season] to Path</strong>. You should see something like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TTJ2y85efy3y2SNDc7S4KA@2x.png" /><figcaption>Default Tableau view on a map after adding some fields.</figcaption></figure><p>Next, add the fields <strong>[Rows] to Rows</strong> and <strong>[Cols] to Columns</strong>. This time, we’ll <strong>do a slightly different table calc configuration</strong>, but it will be the same again for both fields.</p><p>Right click the blue pill and select “Edit Table Calculation”. Select “Specific Dimensions”, make sure <strong>both your [Season] and [Team] are checked</strong>. <strong>Drag the [Team] field to the top</strong> and ensure <strong>“At the level” is also on [Team].</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/341/1*940ZRVuV5vTzJ2Lwa_xsoA@2x.png" /><figcaption>Configuring table calculation for map layers [Rows] and [Cols].</figcaption></figure><p>Again, feel free to add a custom sort in the “Sort order” control if you’d like. Just make sure to apply the same sort logic to both [Rows] and [Cols]. Once you’re done, you should see something like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8ztv9z1Mgt6asgqWz42wGw@2x.png" /><figcaption>Tableau view after editing table calculations.</figcaption></figure><p>Take [Wins per Season Makepoint] and again drag it into the view to <strong>add another marks layer</strong>. This time, change your <strong>Mark Type to Area</strong>, drag <strong>[Team] to Detail</strong>, and <strong>[Season] to Detail</strong>.</p><p>Now that we have 2 map layers, we can go in the top toolbar and select <strong>Map → Background Maps → None</strong>. You should see something like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5HJHmz677cslfgDNpdigsA@2x.png" /><figcaption>Tableau view after removing background maps.</figcaption></figure><p>For the final calculation, I chose to add the team label in the top center of each team’s view.</p><pre>// Team Name Makepoint<br>// We&#39;re just plotting a single dot in space to write our label on<br>// I used 18 because it sits above the tallest wins<br>// I used 0.5 because we normalized our seasons between 0 and 1.<br>MAKEPOINT(18, 0.5)</pre><p>Add [Team Name Makepoint] as another map layer, change the <strong>Mark Type to Circle</strong>, under Color lower the <strong>opacity to 0%</strong>, add <strong>[Team] to Label</strong>, and change the <strong>text alignment to middle center</strong>. You should now see something like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OSGNDpealtyeH2OPTK8cZw@2x.png" /><figcaption>Tableau view after adding team name labels.</figcaption></figure><p>The only thing left to do is clean-up! For layers like [Team Name Makepoint], make sure to click the dropdown and select <strong>“Disable Selection”</strong>. This prevents annoying hover effects from impacting your users.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/291/1*3IzKRrntnYhn_k0eXDSRUQ@2x.png" /><figcaption>How to disable selection on a map layer.</figcaption></figure><p>Then, touch up your axes, lines, headers, colors, and fonts as you wish. My final sheet looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dDqbJfVlyKbpsWbFwHZogg@2x.png" /><figcaption>Result after final clean-up steps.</figcaption></figure><h3>Wrapping It Up</h3><p>See, that wasn’t so bad! Of all the calculations that I think are worth bookmarking and referring back to, [Rows] and [Cols] are 100% my top 2. In fact, I’m going to write them in once more at the bottom of this blog for my own future reference:</p><pre>// Cols<br>(INDEX()-1)%[pCols]<br><br>// Rows<br>INT((INDEX()-1)/[pCols])</pre><p>I hope this tutorial is easy to follow and you learned a new trick to elevate your Tableau visualizations. See you next month!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4233b4dca912" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[FeizaData 2025: Year in Review]]></title>
            <link>https://medium.com/@blakefeiza/feizadata-2025-year-in-review-68e1736071d7?source=rss-5da05b3c7452------2</link>
            <guid isPermaLink="false">https://medium.com/p/68e1736071d7</guid>
            <dc:creator><![CDATA[Feiza Brothers]]></dc:creator>
            <pubDate>Fri, 02 Jan 2026 03:45:50 GMT</pubDate>
            <atom:updated>2026-01-02T22:31:17.461Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*C2CkeLymlwXzBF2SjpLQuw.png" /></figure><p>As I reflect on 2025, it has been a pretty busy year. A lot changed, there were many wins along the way, and the year truly flew by. Since time isn’t going to slow down anytime soon, I think it’s important to pause and reflect every once in a while, and the new year feels like the best time to do that.</p><p>This post will not look like most of my blog content. Instead, I want to take a moment to reflect on a few things from the past year: my favorite vizzes I created in 2025, some of the vizzes from other authors that inspired me, a few of my biggest personal and professional wins, and finally a look ahead at my goals for 2026.</p><h3>10 Vizzes I’m Most Proud Of</h3><h4>Idle Visual Album Poster (January 10, 2025)</h4><p>In 2024, my best friend achieved a huge milestone by releasing his first album on streaming platforms. To commemorate this, I created a data-backed poster visualizing the notes played across each song. This viz was a fun passion project to work on and ended up being one of my favorite pieces I published in 2025.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9fhwM7tRkGZq_4qcTpOPwA.png" /><figcaption><a href="https://public.tableau.com/app/profile/blake.feiza/viz/idlebyyupyiyvisualalbumposterfeizadata/IdleAlbumPoster">https://public.tableau.com/app/profile/blake.feiza/viz/idlebyyupyiyvisualalbumposterfeizadata/IdleAlbumPoster</a></figcaption></figure><h4>South Park #DataPlusMovies (January 20, 2025)</h4><p>As part of the #DataPlusMovies community project between IMDb and Tableau, I wanted to create a South Park-themed viz to participate and explore a show I genuinely enjoy. I spent way too much time in Figma designing the background image (probably longer than the analysis), but the end result made it worth it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*p7BTsIVYvByKDD6fGAszuw.png" /><figcaption><a href="https://public.tableau.com/app/profile/blake.feiza/viz/ComeonDowntoSouthParkDataPlusTV/DATAPLUSTV">https://public.tableau.com/app/profile/blake.feiza/viz/ComeonDowntoSouthParkDataPlusTV/DATAPLUSTV</a></figcaption></figure><h4>Impact of Industrialization by Income Group (March 10, 2025)</h4><p>While this viz isn’t particularly flashy, it’s built on a connected scatterplot that helped surface an interesting insight. By using size along each line to represent time, the x-axis is freed up to focus on the relationship between urban and rural populations across income groups. It‘s a fairly simple approach, but one that’s seldom used.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*23ZtD83ujWPcXd4-VgCdlw.png" /><figcaption><a href="https://public.tableau.com/app/profile/blake.feiza/viz/ImpactofIndustrializationbyIncomeGroupMakeoverMonday/ImpactofIndustrializationbyIncomeGroup">https://public.tableau.com/app/profile/blake.feiza/viz/ImpactofIndustrializationbyIncomeGroupMakeoverMonday/ImpactofIndustrializationbyIncomeGroup</a></figcaption></figure><h4>Work Productivity Benchmarks (March 25, 2025)</h4><p>Map layers were clearly on my mind in March. I spent way too long trying to architect these striped bars (see my <a href="https://medium.com/retail-tableau-user-group/tableau-map-layers-magic-striped-bar-chart-67a4b7f544ea?source=your_stories_outbox---writer_outbox_published-----------------------------------------">tutorial blog post here</a> if you’re curious), but the battery-style effect ended up working really well for this productivity dashboard.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5TY4oJk-CetVYEOGxpFR4Q.png" /><figcaption><a href="https://public.tableau.com/app/profile/blake.feiza/viz/ImpactofIndustrializationbyIncomeGroupMakeoverMonday/ImpactofIndustrializationbyIncomeGroup">https://public.tableau.com/app/profile/blake.feiza/viz/ImpactofIndustrializationbyIncomeGroupMakeoverMonday/ImpactofIndustrializationbyIncomeGroup</a></figcaption></figure><h4>Top Migration Destinations (April 6, 2025)</h4><p>Map layers again? Yes, but this time the technique <em>actually</em> made things easier. I used map layers to build the radar charts, combined them with a trellis layout to create the country grid, and added custom icons for each dimension to better communicate what each section represents. I had a lot of fun with this one.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aoVK3KqSoE3L5NZAl6J9fA.png" /><figcaption><a href="https://public.tableau.com/app/profile/blake.feiza/viz/TopMigrationDestinationsin2025MakeoverMonday/MM2025W14">https://public.tableau.com/app/profile/blake.feiza/viz/TopMigrationDestinationsin2025MakeoverMonday/MM2025W14</a></figcaption></figure><h4>#GamesNightViz Pokédex (May 9, 2025)</h4><p>I created this viz as my submission for the #GamesNightViz “Gotta Viz ’Em All” challenge. I knew I wanted to build something creative for this project, and Tableau animations had been top of mind at the time. By using map layers and manually tracing artwork for 20 different Pokémon, I was able to create a fun animated effect that really brought the dashboard to life. Definitely one of the more enjoyable builds from the year.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FOUn_hSaXI0sMrw0RlKqzg.png" /><figcaption><a href="https://public.tableau.com/app/profile/blake.feiza/viz/PokdexTableauEditionGamesNightViz/Pokdex">https://public.tableau.com/app/profile/blake.feiza/viz/PokdexTableauEditionGamesNightViz/Pokdex</a></figcaption></figure><h4>Cycles of Joblessness (August 20, 2025)</h4><p>When this Makeover Monday dataset was released, I wasn’t particularly excited. I didn’t know much about UK history, and at first glance the unemployment trend just looked like a random line chart. With some help from ChatGPT, I dug into the historical context and found anecdotes that helped explain why unemployment was rising or falling at different points in time. Combined with some Figma magic, the final result came together better than I expected.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bSSKQ_oTgerLH_NnfuflCg.png" /><figcaption><a href="https://public.tableau.com/app/profile/blake.feiza/viz/CyclesofJoblessnessMakeoverMonday/MakeoverMonday">https://public.tableau.com/app/profile/blake.feiza/viz/CyclesofJoblessnessMakeoverMonday/MakeoverMonday</a></figcaption></figure><h4>365 Days of Play (August 27, 2025)</h4><p>So… map layers, again! This viz was inspired by a TikTok, and I took it as a challenge to recreate it in Tableau. I also wrote a blog post on this one, <a href="https://medium.com/dataai/behind-the-build-sports-radial-calendar-0ba37ee94910?source=your_stories_outbox---writer_outbox_published-----------------------------------------">check it out here</a> if you’re curious. Overall, this was one of the hardest things I built all year, but easily one of the most rewarding.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-iU2MHh8PpEEsLjAv10nVg.png" /><figcaption><a href="https://public.tableau.com/app/profile/blake.feiza/viz/365DaysofPlay/RadialSportsCalendar">https://public.tableau.com/app/profile/blake.feiza/viz/365DaysofPlay/RadialSportsCalendar</a></figcaption></figure><h4>Drug Harm in the UK (September 12, 2025)</h4><p>Once again, this chart type was not found on the “Show Me” shelf… Playing on the idea of getting “high,” I used a sigmoid function to create a droopy balloon-style chart for some of the most common drug types in the Makeover Monday dataset. This one was a fun experiment that panned out pretty well!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JKZbwNgVMbWJSfJjG4PWmA.png" /><figcaption><a href="https://public.tableau.com/app/profile/blake.feiza/viz/DrugHarmintheUKMakeoverMonday/Feiza">https://public.tableau.com/app/profile/blake.feiza/viz/DrugHarmintheUKMakeoverMonday/Feiza</a></figcaption></figure><h4>F1’s Fiercest Rivalry (September 17, 2025)</h4><p>For a long-form viz, this one came together fairly quickly. As part of a case study, I had the opportunity to showcase my Tableau skills through a data project. I built out a narrative using a handful of chart types to tell the story of a rivalry between two F1 drivers, and it ultimately played a role in helping me land a new job.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/804/1*gS7Dskc9fZM3XJPnAf8WJw.png" /><figcaption><a href="https://public.tableau.com/app/profile/blake.feiza/viz/Formula1sGreatestRivalry/F1">https://public.tableau.com/app/profile/blake.feiza/viz/Formula1sGreatestRivalry/F1</a></figcaption></figure><h3>10 Vizzes That Inspired Me</h3><p>There’s no community that shares work quite like the Tableau community. I wanted to take a moment to highlight a handful of vizzes from 2025 that inspired me throughout the year, whether through their design choices, storytelling, or technical execution.</p><h4>Kings of Confection by Brian Moore (January 24, 2025)</h4><p>This viz makes me salivate a bit. Brian Moore does a fantastic job telling the story of candy in America through a long-form viz, and the design completely blew me away. I really enjoyed reading through this dashboard and learned a few things along the way too!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MBonhfYj3YxagHwd9tRlRA.png" /><figcaption><a href="https://public.tableau.com/app/profile/brian.moore7221/viz/KingsofConfection/KingsofConfection">https://public.tableau.com/app/profile/brian.moore7221/viz/KingsofConfection/KingsofConfection</a></figcaption></figure><h4>The Lions Prey by Chris Westlake (June 16, 2025)</h4><p>When Chris Westlake publishes a new viz, you already know it’s going to be worth your time. This rugby dashboard caught my eye immediately and does an excellent job encoding a ton of information into a single shape. The spirals are especially fascinating to me, and I’d love to use this as inspiration for a future viz.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hOgJHwRI7sENEAGjMD57MQ.png" /><figcaption><a href="https://public.tableau.com/app/profile/westlake.cjw/viz/TheLionsPrey_17500907162260/TheLionsPrey">https://public.tableau.com/app/profile/westlake.cjw/viz/TheLionsPrey_17500907162260/TheLionsPrey</a></figcaption></figure><h4>Super Mario Kart by Matt Huff (June 28, 2025)</h4><p>I really enjoyed Matt’s approach to this #GamesNightViz Mario project. His use of map layers to build these charts is clever and works really well in this dashboard. Combined with a strong themed design, this dashboard stands out as one of my favorites from the year.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SS8h3KhbYuh6b0qw1SozWg.png" /><figcaption><a href="https://public.tableau.com/app/profile/matt.huff/viz/SuperMarioKart/Dashboard1">https://public.tableau.com/app/profile/matt.huff/viz/SuperMarioKart/Dashboard1</a></figcaption></figure><h4>Corruption Perceptions Index by Zyad Wael (August 7, 2025)</h4><p>Week over week, Zyad is one of the creators I most look forward to seeing participate in Makeover Monday. I liked this dashboard so much that I actually tried to recreate the design the following week with a similar dataset. A great example of a clean, effective dashboard.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Mt_OFY9ukBvbXooiocAoDw.png" /><figcaption><a href="https://public.tableau.com/app/profile/zyad.wael/viz/CorruptionPerceptionsIndex-MakeoverMondayWeek32/Dashboard">https://public.tableau.com/app/profile/zyad.wael/viz/CorruptionPerceptionsIndex-MakeoverMondayWeek32/Dashboard</a></figcaption></figure><h4>Satisfying the appetite for sustainable seafood by Brittany Roseneau (August 23, 2025)</h4><p>Brittany’s long-form viz is one of my favorite examples of storytelling from 2025. I love the natural progression of the narrative, and the design and chart choices complement the story perfectly from start to finish.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6pFj38A_qHVSgpSJf97eUA.png" /><figcaption><a href="https://public.tableau.com/app/profile/brrosenau/viz/Satisfyingtheappetiteforsustainableseafood/Satisfyingtheappetiteforsustainableseafood">https://public.tableau.com/app/profile/brrosenau/viz/Satisfyingtheappetiteforsustainableseafood/Satisfyingtheappetiteforsustainableseafood</a></figcaption></figure><h4>Stock Performance by Seun Adeyemo (September 28, 2025)</h4><p>I’m still impressed that Seun took the time to learn the algorithm Tableau uses to render its default tree chart. With that knowledge, he was able to build a fully custom tree diagram with rounded corners, custom labels, and beautiful animations when filters are toggled. So cool!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-NE85etqhEFdkZmkvCxQEA.png" /><figcaption><a href="https://public.tableau.com/app/profile/bigxpt/viz/STOCKPERFOMANCE/unique">https://public.tableau.com/app/profile/bigxpt/viz/STOCKPERFOMANCE/unique</a></figcaption></figure><h4>Pawnee Parks and Recreation by Lisa Trescott (October 4, 2025)</h4><p>I’ve never seen a Tableau dashboard that feels so much like a real website. Once again, Lisa completely blows me away with her Tableau skills and delivers an incredibly well-designed exploratory dashboard that feels so unique.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zNpEKE2tsX7gmoo3a_Zlaw.png" /><figcaption><a href="https://public.tableau.com/app/profile/lisa.trescott/viz/PawneeParksandRecreation/Home">https://public.tableau.com/app/profile/lisa.trescott/viz/PawneeParksandRecreation/Home</a></figcaption></figure><h4>Mastering Tableau Series (#17) by Rubén Martínez (November 10, 2025)</h4><p>Rubén has over 20 dashboards in his “Mastering Tableau” series, and each one is chef’s kiss. They aren’t only beautifully designed, but they also include clear explanations for advanced techniques, chart types, and core fundamentals. Every time he posts a new one, I’m excited to see what he’s sharing next!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0Sn5BERrStWx2RrarugWaQ.png" /><figcaption><a href="https://public.tableau.com/app/profile/rub.nm/viz/MasteringTableau17_Tablecalculations2-Windowcomparisons/Dashboard">https://public.tableau.com/app/profile/rub.nm/viz/MasteringTableau17_Tablecalculations2-Windowcomparisons/Dashboard</a></figcaption></figure><h4>Playing with Food by Louis Yu (November 18, 2025)</h4><p>No matter the topic, Louis always finds a way to tie the Iron Viz theme back to video games. This dashboard is another great example of that! I really enjoyed the interactivity and design choices throughout this dashboard and found myself clicking around all the knobs and buttons.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Se7cFh2PpfBDzeerGnpFng.png" /><figcaption><a href="https://public.tableau.com/app/profile/louisyu/viz/PlayingwithFoodIronViz/Evolution">https://public.tableau.com/app/profile/louisyu/viz/PlayingwithFoodIronViz/Evolution</a></figcaption></figure><h4>Nutrition, Energy Sources, and Ingredients of Dim Sum by Kevin Wee (November 21, 2025)</h4><p>When Iron Viz season came around, Kevin was one of the artists I was most excited to see. After last year’s Broadway-themed qualifier submission, I was really curious to see what he would come up with this year. The design and chart choices in this dashboard are a true masterclass!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0lQkOnaBW4U4IWNB79HyLA.png" /><figcaption><a href="https://public.tableau.com/app/profile/kevin.wee/viz/NutritionValuesofDimSumIronVizedition/Dashboard">https://public.tableau.com/app/profile/kevin.wee/viz/NutritionValuesofDimSumIronVizedition/Dashboard</a></figcaption></figure><h3>Reflection: Wins in 2025</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*uhx4YgHHnXD1RQuA" /><figcaption>Photo by <a href="https://unsplash.com/@sagarp7?utm_source=medium&amp;utm_medium=referral">Sagar Patil</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>I had some big wins and checked off some goals this year. One of those goals was to publish a blog post every month in 2025. If you are reading this, I’m so thankful for your time. It makes the whole writing experience worth it.</p><p>As I reflected on the year, a few other themes stood out:</p><h4>TC25</h4><p>I attended my second Tableau Conference this year in San Diego and had the opportunity to speak for the first time alongside Matt Huff. It was an incredible experience, and seeing a standing-room turnout was something I will never forget. We also led an in-person meetup for the Retail and Consumer Goods Tableau User Group with Ojoswi Basu. It was a great experience all around, and I am already looking forward to building on this momentum at TC26.</p><h4>Community Involvement</h4><p>2025 was the first full year of the #RetailTUG. We hosted 6 events this year with some incredible guest speakers, including one hybrid session. The user group continues to trend upward, and I am excited to keep delivering strong, relevant content in the year ahead.</p><p>Toward the end of the year, I also announced that I will be joining the #MakeoverMonday team. As a frequent participant in this project over the past few years, I am incredibly grateful for the opportunity and excited to help curate datasets each week that could use a “makeover”.</p><p>Lastly, I am proud to share that I was named both a 2025 DataFam Rising Star and a 2025 Tableau Ambassador. Both of these are community-nominated recognitions, and it means a lot to be acknowledged by a group I have looked up to for years. I plan to keep giving back in 2026!</p><h4>Professional Development</h4><p>I am incredibly happy with the progress I’ve made with Tableau, and a large part of that growth is thanks to Next-Level Tableau. I have been training with Andy Kriebel for the past two years and am excited to continue that journey in 2026. His mentorship has been invaluable as I’ve grown technically and expanded my network.</p><p>This year also marked a major career transition for me, as I moved from HEB to a role at Apple as a Senior Data Visualization Engineer. It has been an exciting next step, and I’m grateful for the experiences that led me here.</p><h3>Goals Going Into 2026</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*VzixlWZeVQ2n0lUi" /><figcaption>Photo by <a href="https://unsplash.com/@glenncarstenspeters?utm_source=medium&amp;utm_medium=referral">Glenn Carstens-Peters</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Even though there is some well-documented psychological research suggesting that sharing your goals can actually make you less likely to achieve them (the short version being that our brains release a sense of “success” just by saying them out loud), there are still a few things I want to call out as I look ahead to 2026.</p><h4>Continuing to Blog Monthly on FeizaData</h4><p>I’ve found that publishing one blog post a month is a pretty healthy cadence. It gives me enough time to be thoughtful about each post while still staying consistent. I still have plenty of ideas I want to explore here and have not run out of gas yet. :)</p><h4>Progressing as a Community Leader</h4><p>Through blogging, creating vizzes, speaking at TC26, and continuing to lead community user groups and projects, I hope to keep growing as a community leader in the Tableau and data visualization space. More than anything, I want to stay engaged, keep learning, and help create spaces where others feel encouraged to share their work as well.</p><h4>Trying Something New?</h4><p>I have a tendency to sign myself up for more than I can chew (I even signed up to run a 50K in May…) At some point in 2026, I would like to try my hand at livestreaming some viz sessions. Makeover Monday feels like a natural place to experiment with this, using a timeboxed format to build something each week. It would be similar in spirit to Andy Kriebel’s old “Watch Me Viz” series on YouTube. We’ll see if I actually commit to the idea now that it’s in writing…</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/0*JWZwQybnmHVfTUF9.jpg" /><figcaption><a href="https://www.youtube.com/watch?v=AB61I9VK7Xs">Into The Wild | Happiness Only Real When Shared — YouTube</a></figcaption></figure><p>To wrap up the year and this post, I just want to say thank you if you’re reading this. As much as it makes me happy to write posts about topics that excite me, it means the world to know there are people on the other end that are consuming it. I hope you stick with me in 2026!</p><p>-Blake Feiza</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=68e1736071d7" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Easiest DZV Tutorial of All Time]]></title>
            <link>https://medium.com/@blakefeiza/the-easiest-dzv-tutorial-of-all-time-a81359dcb088?source=rss-5da05b3c7452------2</link>
            <guid isPermaLink="false">https://medium.com/p/a81359dcb088</guid>
            <category><![CDATA[datafam]]></category>
            <category><![CDATA[data-visualization]]></category>
            <category><![CDATA[dynamic-zone-visibility]]></category>
            <category><![CDATA[tableau]]></category>
            <dc:creator><![CDATA[Feiza Brothers]]></dc:creator>
            <pubDate>Tue, 02 Dec 2025 03:17:41 GMT</pubDate>
            <atom:updated>2025-12-02T03:17:41.183Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fbLQ8xdeWcue9Cdo0UaGUA.png" /><figcaption>The Easiest DZV Tutorial of All Time</figcaption></figure><p>I remember early in my Tableau journey hearing about Dynamic Zone Visibility (DZV) and thinking it must be some pretty advanced stuff.</p><p>I’m writing to prove to you it’s actually one of the easiest interactivity features in Tableau.</p><p>I’ll break the process into 3 easy steps and mention some common misconceptions along the way. If you’re already familiar with DZV, I’ll also add an advanced technique that might solve that “impossible” problem you’ve been stuck on.</p><h3>DZV in 3 (Simple Steps)</h3><h4>1. Create a Parameter</h4><p>In all DZV use cases, the first step is to create a parameter. The type doesn’t matter here, because step 2 will reference this parameter inside a calculation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*wDDPwg3USJysJjEaGz67WA.gif" /><figcaption>Step 1. Create a Parameter</figcaption></figure><h4>2. Create a Calculated Field</h4><p>A parameter behaves like a light switch with no wiring by itself. It doesn’t control anything until we pair it with a calculation. For DZV to work, we need a calculated field that evaluates to exactly TRUE or FALSE across the entire workbook.</p><p>The key idea is that parameters always return a single value globally. That makes them perfect for DZV. Filters do not work here because they can vary row by row or sheet by sheet.</p><p>In this example, I create a simple calculated field like this:</p><pre>// DZV. Show Chart<br>[DZV Parameter] = &#39;Show&#39;</pre><p>Observe that this calculation will always evaluate to either TRUE or FALSE. Also, because parameters always have one global value, this calculation’s result will always be ONLY TRUE or ONLY FALSE at any time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*OwyoOQV7xhnYRvvcuYBOKg.gif" /><figcaption>Step 2. Create a Calculated Field</figcaption></figure><blockquote>Note that in this example, my [DZV Parameter] has 3 values; “Show”, “Hide”, and “Etc”. The calculation I’m using will show the chart when the parameter is “Show” and hide for all other values. We can modify the logic here in a couple ways depending on the goal:</blockquote><pre>// Only Show the Chart on &quot;Show&quot;<br>[DZV Parameter] = &quot;Show&quot;<br><br>// Only Hide the Chart on &quot;Hide&quot;<br>[DZV Parameter] != &quot;Hide&quot;<br><br>// Show the chart for a specific set of options<br>[DZV Parameter] IN (&quot;Show&quot;, &quot;Etc&quot;)</pre><h4>3. Edit on Dashboard</h4><p>Now that our calculation from Step 2 exists, we don’t even need to add it anywhere. Tableau simply stores it in the workbook and recognizes it as a “DZV-eligible” field. We can head to our dashboard and reference it directly.</p><p>DZV is a property you can apply to any dashboard object. To modify this property, select an object, then go to <strong>Layout &gt; Control visibility using value</strong>.</p><p>If your calculated field was set up correctly, it will appear in the DZV drop-down. If it does not appear, revisit the calculation; this is usually because the logic doesn’t resolve to either TRUE or FALSE across the entire workbook.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*IARsrxPzJ4x0KtT--rrs5A.gif" /><figcaption>Step 3. Edit on Dashboard</figcaption></figure><p>If everything works correctly, you’re done! You can add as many calculations as you need to cover different user experience scenarios, and you can apply the same calculation to any number of objects or dashboards.</p><p>One final note: if you are using a Boolean parameter, you can actually connect DZV directly to the parameter without creating a calculated field. This only applies to true Boolean parameter types, but it is a helpful technical shortcut to know.</p><h3>Bonus Technique: When “Easy DZV” Won’t Cut It</h3><p>Sometimes, it can be surprisingly difficult to create a calculation that is globally TRUE or FALSE and still meet the requirements for your DZV use case. To show what I mean, here is an example from Superstore:</p><p>Suppose I want to look at the number of yearly orders by ship mode for a selected manufacturer. The dashboard below shows the result for AT&amp;T, and everything looks fine because there are orders in every ship mode.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*B4NmrduhVEC6NnVvsXPCJQ@2x.png" /><figcaption>Bonus Technique with all 4 sheets available.</figcaption></figure><p>When I switch my [Manufacturer Parameter] to Ativa, there are no First Class orders. This leaves an empty space in the layout where that bar would have been, and I would prefer that First Class disappear entirely so the remaining ship modes have more room.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*y4GBZNLfLiUd1wNaYOywbw@2x.png" /><figcaption>Bonus Technique with sheet missing data.</figcaption></figure><p>This is where DZV becomes a bit trickier.</p><p>To control visibility for each ship mode independently, we need calculated fields (one for each ship mode) that always resolve to TRUE or FALSE across the entire workbook. These calculations need to understand whether the selected manufacturer has at least one qualifying record.</p><p>The key is using fixed LODs.</p><p>I created the following calculations (one for each of my four ship modes):</p><pre>// DZV. First Class Orders?<br>{ COUNTD(<br>    IF [Manufacturer] = [Manufacturer Parameter] AND [Ship Mode] = &#39;First Class&#39;<br>    THEN [Order ID] <br>    END<br>  ) &gt; 0 <br>}<br><br>// DZV. Same Day Orders?<br>{ COUNTD(<br>    IF [Manufacturer] = [Manufacturer Parameter] AND [Ship Mode] = &#39;Same Day&#39;<br>    THEN [Order ID] <br>    END<br>  ) &gt; 0 <br>}<br><br>// DZV. Second Class Orders?<br>{ COUNTD(<br>    IF [Manufacturer] = [Manufacturer Parameter] AND [Ship Mode] = &#39;Second Class&#39;<br>    THEN [Order ID] <br>    END<br>  ) &gt; 0 <br>}<br><br>// DZV. Standard Class Orders?<br>{ COUNTD(<br>    IF [Manufacturer] = [Manufacturer Parameter] AND [Ship Mode] = &#39;Standard Class&#39;<br>    THEN [Order ID] <br>    END<br>  ) &gt; 0 <br>}</pre><p>Each ship mode chart gets its own calculation that checks whether there is at least one [Order ID] matching both the manufacturer and the ship mode requirements. If there is at least one match, the calculation returns TRUE. If not, it returns FALSE.</p><p>By wrapping the logic in a FIXED LOD, the outcome becomes global and stable, which is exactly what DZV requires.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*qAp785l_lHxl_pDd3auz5w.gif" /><figcaption>Adding Bonus Technique DZV Calcs for solution.</figcaption></figure><p>Variations of this pattern have solved several otherwise challenging layout problems for me, and I hope it helps you with yours.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a81359dcb088" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Tableau Trail Tips #2]]></title>
            <link>https://medium.com/@blakefeiza/tableau-trail-tips-2-9f73beb50cf1?source=rss-5da05b3c7452------2</link>
            <guid isPermaLink="false">https://medium.com/p/9f73beb50cf1</guid>
            <category><![CDATA[tableau]]></category>
            <dc:creator><![CDATA[Feiza Brothers]]></dc:creator>
            <pubDate>Sat, 01 Nov 2025 05:01:57 GMT</pubDate>
            <atom:updated>2025-11-01T05:02:53.677Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qNb0LPvEoLG4-EuE6iMUtw.png" /><figcaption>Tableau Trail Tips #2</figcaption></figure><p>For this month’s blog post, I’m excited to revisit the Trail Tips mini-series and share another set of my favorite Tableau tips. I always enjoy this type of content because it’s always those small, simple tips that end up making a huge difference in my workflow. Hopefully, one of the ten below does the same for you!</p><p>Before we begin, a special thanks to <a href="https://www.linkedin.com/in/mrpaulalbert/">Paul Albert</a> for contributing several of these tips following the first edition. If you have any tips of your own, feel free to share in the comments or reach out via LinkedIn!</p><h3>Tip #1: Edit Default Properties on Your Fields in Tableau</h3><p>There’s nothing better than inheriting a workbook full of acronyms and unfamiliar field names only the original developer could decode. Did you know Tableau gives us a way to leave field-level documentation for whoever explores that workbook next?</p><p>Right-click any field in the data pane and go to <strong>Default Properties → Comment</strong>. Here, you can notes or logic tied to the data. When someone hovers over that field, your comment will appear as a tooltip to help explain what’s going on.</p><p>As we move towards an increasingly agentic, AI-assisted future, these small acts of documentation provide semantics for both human and model-based developers.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*SFcriI43ZY0GLPUSMtQKcQ.gif" /><figcaption>Tip #1: Edit Default Properties on Your Fields in Tableau</figcaption></figure><h3>Tip #2: Editing Default Aggregation for Measures</h3><p>Tableau also lets you control the default aggregation type it applies when you drag a measure into the view. By default, Tableau assumes <strong>SUM</strong> is the appropriate aggregation.</p><p>Right-click your field and select <strong>Default Properties → Aggregation</strong> to change it. For example, setting <strong>AVG</strong> as the default for the [Discount] field makes more sense than summing those values.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*FfLR9WzdoNLWtrmPydG-AA.gif" /><figcaption>Tip #2: Editing Default Aggregation for Measures</figcaption></figure><h3>Tip #3: Edit Default Fiscal Year Start for Date Fields</h3><p>Here’s another awesome default property that applies specifically to date fields. By default, Tableau assumes your fiscal year runs from January to December, but that’s often not the case. Did you know you can edit the Fiscal Year Start month from the same menu?</p><p>Right-click a date field and select <strong>Default Properties → Fiscal Year Start</strong> to align your fiscal calendar correctly. Once updated, Tableau will automatically respect your fiscal year setting.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*GeJYEqSwU1BT4PqEur9GfQ.gif" /><figcaption>Tip #3: Edit Default Fiscal Year Start for Date Fields</figcaption></figure><h3>Tip #4: Using Describe to Fetch Field Values</h3><p>There’s nothing worse than manually typing long string values into an IF or CASE statement in Tableau. You’re prone to typos and the task can take a pretty long time.</p><p>Right-click your field and select <strong>Describe…</strong> to open a summary window. Copy the domain of values and paste them directly into your calculation. You’ll still need to format them in your calculation, but the copy/paste approach feels much easier than typing them from scratch.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*8dOps9y1iP-3wSybaOMjyQ.gif" /><figcaption>Tip #4: Using Describe to Fetch Field Values</figcaption></figure><h3>Tip #5: Quickly Select Aggregation When Dragging a Field into the View</h3><p>When you drag a measure into the view, Tableau automatically applies its default aggregation. There is a nice trick to specify the aggregation while dragging the pill out.</p><p>On <strong>Mac</strong>, hold <strong>Option</strong> and drag the field into the view. On <strong>Windows</strong>, use <strong>Right-click + Drag</strong>. Tableau will pop up a small menu letting you pick from the list of aggregations before you drop the field.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*tPf2HjRJD85KhMp4-H44ug.gif" /><figcaption>Tip #5: Quickly Select Aggregation When Dragging a Field into the View</figcaption></figure><h3>Tip #6: Using Colored Tabs to Categorize Sheets and Stay Organized</h3><p>As workbooks grow, keeping track of all your sheets can quickly get overwhelming. A helpful way to stay organized is by assigning colors to your sheet tabs.</p><p>Right-click a sheet tab at the bottom of the workbook and select <strong>Color</strong>. You can use consistent colors to group worksheets, dashboards, or stories with related content. I find this makes navigating complex workbooks much easier.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*WJmrYh3pd16tXDe8POAkkg.gif" /><figcaption>Tip #6: Using Colored Tabs to Categorize Sheets and Stay Organized</figcaption></figure><h3>Tip #7: Control How Sheets Are Displayed</h3><p>Did you know Tableau gives three different options to navigate your workbook? In the bottom-right corner of the workspace, you can toggle between <strong>tabs</strong>, <strong>filmstrip</strong>, or <strong>sheet sorter</strong> views. The filmstrip is great for quick thumbnail scanning, and the sheet sorter gives you a nice, visual overview of the entire workbook.</p><p>Combine this with colored tabs from Tip #6 to keep your workflow quick and organized.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*0US0KArD9MkIHidpFrfhgw.gif" /><figcaption>Tip #7: Control How Sheets Are Displayed</figcaption></figure><h3>Tip #8: Check/Uncheck Multiple Items at Once</h3><p>When working with dropdown filters or parameters, you don’t need to click each value one by one.</p><p>Select the first item, hold <strong>Shift</strong>, and click the last item in the list to highlight a range. Then, either click one checkbox or press <strong>Spacebar</strong> to check or uncheck all the options at once.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*urQWG1nZp6ExXfBQw1BOaw.gif" /><figcaption>Tip #8: Check/Uncheck Multiple Items at Once</figcaption></figure><h3>Tip #9: Remove the Grey Highlight from a Dropdown</h3><p>Have you ever battled with a filter or parameter dropdown because you wanted users to start with a clean, fresh, untouched list? The grey highlight can make it look like a choice has already been made.</p><p>To remove it, hold <strong>CMD (Mac)</strong> or <strong>CTRL (Windows)</strong> and click the highlighted item. The selection will clear and leave your dropdown clean.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*KCM3nFGeMKhf26tRd64NwQ.gif" /><figcaption>Tip #9: Remove the Grey Highlight from a Dropdown</figcaption></figure><h3>Tip #10: Rename a Field by Clicking and Holding</h3><p>Instead of right-clicking a field and selecting <strong>Rename</strong>, try just clicking and holding on the field name. After a moment, Tableau will let you edit it directly.</p><p>I actually discovered this one by accident and now it’s my preferred way to rename fields.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*R4yezIA7fJQ-i3aMpO_Ueg.gif" /><figcaption>Tip #10: Rename a Field by Clicking and Holding</figcaption></figure><p>And that wraps up Tableau Trail Tips #2. I hope that at least one of these time-savers can find its way into your personal Tableau workflow. Also, let me know your favorite Tableau tricks in the comments! I’d love to hear it, and/or feature it in the next post.</p><p>Happy Vizzing!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9f73beb50cf1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Making Over “Makeover Monday”]]></title>
            <link>https://medium.com/dataai/making-over-makeover-monday-e7d440813c18?source=rss-5da05b3c7452------2</link>
            <guid isPermaLink="false">https://medium.com/p/e7d440813c18</guid>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[datafam]]></category>
            <category><![CDATA[tableau]]></category>
            <category><![CDATA[databricks-free-edition]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Feiza Brothers]]></dc:creator>
            <pubDate>Wed, 01 Oct 2025 05:01:53 GMT</pubDate>
            <atom:updated>2025-10-02T20:21:43.575Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tLTKmYbHcwzzJv162WZAlg.png" /><figcaption>#FeizaData Thumbnail.</figcaption></figure><p>If you’ve been around the DataFam for any length of time, you’ve probably heard of <a href="https://makeovermonday.co.uk/">Makeover Monday</a>. It’s a weekly challenge that provides a new dataset every week and invites you to reimagine the story it tells. For the last 2 years, I’ve used this community project as a commitment device to hold me accountable to practicing Tableau. Over time, those quick projects start to stack up into a portfolio of ideas I can share or revisit myself later.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/531/1*Fis73Mdstq4hjcIeiwMpPQ.png" /><figcaption>Makeover Monday community project logo.</figcaption></figure><p>For a recent challenge (<a href="https://www.kaggle.com/datasets/sahilislam007/ai-impact-on-job-market-20242030?">2025 Week 37</a>), the dataset focused on AI’s impact on the job market. The topic felt immediately relevant, and I was excited to boot up Tableau and dig into the data.</p><p>That excitement didn’t last long, though. While initially exploring the records, a few things stuck out to me.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/530/1*-HqGpwyRdCPpBNXjDBj8Ig.png" /><figcaption>Sample records from the Makeover Monday dataset.</figcaption></figure><ul><li>An economist with an associate’s degree earning $150K? In the retail industry? Apparently, I picked the wrong career path…</li><li>A dentist categorized in the entertainment industry? That sounds like an Impractical Jokers bit.</li><li>I’m still not sure what kind of fitness manager belongs in the transportation industry.</li></ul><p>The more I explored, the harder it was to trust any patterns I saw. I even went back to the Kaggle dataset to see what the author (and others) had to say.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DllS-AyJKms_pFFC7bxv9w.png" /><figcaption>Dataset author’s response to a user’s Kaggle comment.</figcaption></figure><p>…I think we might have a slightly different perspective on how far from “factual” a synthetic dataset should deviate. Nevertheless, I was stuck. I didn’t want to create and share something misleading, especially with a topic I felt passionate about.</p><p>What if I used this dataset as a starting point and rebuilt the fields myself? Not by hand, of course, but with help from our good pal AI.</p><h3>Rebuilding the Dataset with Databricks Community</h3><p>A few weeks ago, I heard some updates had been made to <a href="https://accounts.cloud.databricks.com/">Databricks’ Free Edition</a> and offered daily free compute to users. I knew they had some AI features baked into their ecosystem, and I’ve been wanting to give them a shot. This project became the perfect opportunity. Here’s how I got started:</p><h4>Importing the Dataset</h4><ul><li>On the left side of the Databricks workspace, go to Data Ingestion &gt; Create or modify table.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l-mPeM0v7MmmQsoWMx9ZEQ.png" /><figcaption>Navigating to the Databricks file upload page.</figcaption></figure><ul><li>Drag and drop your CSV (in this case, the Makeover Monday data). I created a schema called tableau and named my table ai_job_trends.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*krptNIYKbDBB-cggmnhQ5g.png" /><figcaption>Page to assign a schema, table name, and rename columns before creating the table.</figcaption></figure><blockquote>Column renaming is an option here, but I left that for later. The goal was just to get the data staged and ready.</blockquote><h4>Setting Up the Notebook</h4><p>I created a new notebook and ran my standard boilerplate imports.</p><pre># Set up Spark and Python Environment<br>from pyspark.sql.functions import *<br>from pyspark.sql.types import *<br>from pyspark.sql import Window<br>from datetime import date, datetime, timedelta</pre><p>For context, I tend to work more regularly in PySpark, so this blog post mixes PySpark and SQL quite a bit. It’s not the only way to transform your data, but this rhythm helps me move quickly.</p><p>To create the foundation for my new dataset, I first wanted to know how many job titles were actually in the data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*07RcpOqqkzyktIzAcXk7ng.png" /><figcaption>Checking distinct job titles in the dataset.</figcaption></figure><p>After removing duplicates, I landed on 639 unique job titles (compared to the nearly 30,000 records in the original dataset). This single-column dataframe became the spine for my AI prompts to follow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7L7PySLP8A0SI-FuKBcJnw.png" /><figcaption>Creating a temporary view for SQL querying.</figcaption></figure><h4>Using AI to Rebuild the Metadata</h4><p>The goal was to generate accurate fields for each job title, including the correct industry, the required education level, and other context from the original dataset. I wanted to turn the simple list of jobs into a structured table that conceptually matched the original Kaggle data, but told the accurate story that we were missing.</p><p>With the isolated 639 job titles, it was time to explore Databricks’ <a href="https://docs.databricks.com/aws/en/sql/language-manual/functions/ai_query"><strong><em>ai_query()</em></strong></a> function. After a few iterations, I found an approach that would generate all the fields I wanted (i.e., industry, education, AI impact level) in a single column. The structured output would run quickly and yield data that I could easily split in a subsequent step.</p><p>The final prompt I landed on consists of a few key learnings and decisions I made along the way:</p><ul><li><strong>Use numbered/bulleted instructions</strong>: LLMs tend to perform better with structured steps, so I used them wherever possible.</li><li><strong>Provide strict formatting rules and examples</strong>: This reduced noise in the responses and ensured results were predictable and usable.</li><li><strong>Separate attributes with pipes (|)</strong>: Condensing all the attributes into a single pipe-delimited column significantly improved runtime compared to running a separate <strong><em>ai_query()</em></strong> for each attribute.</li><li><strong>Constrain industry categories</strong>: Confining “Industry” to a fixed set of eight kept categories consistent, but some jobs stretched those boundaries. There’s no perfect approach here, but I opted to stay close to the original dataset.</li></ul><p>Here’s the query (including the prompt) that I used to generate my new data:</p><pre>%sql<br>SELECT <br>    job, <br>    ai_query(<br>        &quot;databricks-meta-llama-3-3-70b-instruct&quot;,<br>        CONCAT(<br>            &quot;You are a structured data generator.<br>            Given a job title in the United States, return exactly 6 attributes separated by the pipe character (|). <br>            The output must contain no explanations, labels, or extra words — only the 6 attributes in order. <br>            <br>            Attributes (always return in this order):<br>            1. Industry → choose ONE from {Retail, Education, Manufacturing, IT, Finance, Transportation, Entertainment, Healthcare}.<br>            2. Job Status → choose ONE from {Increasing, Decreasing}. Base this on whether demand is rising or falling due to AI expansion.<br>            3. AI Impact Level → choose ONE from {High, Medium, Low}. Reflect the degree of AI disruption on this role.<br>            4. Median Salary (numeric USD, no symbols or commas, e.g., 55000).<br>            5. Experience Required (integer years, e.g., 5).<br>            6. Required Education → choose ONE from {High School, Associate, Bachelor Degree, Master Degree, PhD}.<br><br>            Formatting rules:<br>            - Separate attributes ONLY with &#39;|&#39;.<br>            - Do NOT include units, words like &#39;USD&#39; or &#39;years&#39;.<br>            - Do NOT add any commentary, explanation, or text before/after the output.<br>            - Output must be exactly one line.<br><br>            Example valid output:<br>            Entertainment|Increasing|Medium|55000|5|Bachelor Degree<br><br>            Job Title: &quot;,<br>            CAST(`job` AS STRING)<br>        )<br>    ) AS ai_metadata<br>FROM jobs</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*muVU4sZAtuXLyqwqFFIF4w.png" /><figcaption>Running the AI prompt query in Databricks and previewing output.</figcaption></figure><p>And the code I used to split the pipe-delimited output column into something easier to work with:</p><pre>improved_df = (<br>    _sqldf<br>    .withColumn(&#39;industry&#39;, split(col(&#39;ai_metadata&#39;), &#39;\|&#39;).getItem(0))<br>    .withColumn(&#39;job_status&#39;, split(col(&#39;ai_metadata&#39;), &#39;\|&#39;).getItem(1))<br>    .withColumn(&#39;ai_impact_level&#39;, split(col(&#39;ai_metadata&#39;), &#39;\|&#39;).getItem(2))<br>    .withColumn(&#39;median_salary&#39;, split(col(&#39;ai_metadata&#39;), &#39;\|&#39;).getItem(3))<br>    .withColumn(&#39;experience_required&#39;, split(col(&#39;ai_metadata&#39;), &#39;\|&#39;).getItem(4))<br>    .withColumn(&#39;required_education&#39;, split(col(&#39;ai_metadata&#39;), &#39;\|&#39;).getItem(5))<br>    .drop(&#39;ai_metadata&#39;)<br>)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fWDDwcrLgbYFC6zeEdVqFw.png" /><figcaption>Resultant dataframe after splitting the metadata column.</figcaption></figure><p>This output was solid, but I didn’t want to stop here. How could I be so sure that these new records were accurate? I couldn’t trust the original synthetic data. Why should I blindly trust this newly generated table?</p><p>To strengthen confidence, I created a second <a href="https://docs.databricks.com/aws/en/sql/language-manual/functions/ai_query"><strong><em>ai_query()</em></strong></a> to serve as a validation agent. This step checked every row’s results and flagged any anomalies based on rules I outlined. There are a few things I found especially helpful when writing the validation prompt:</p><ul><li><strong>Forcing a simple output</strong>: Being explicit about returning exactly one character (“1” or “0”) ensured the output was easy to use.</li><li><strong>General validation rules</strong>: These caught obvious issues like invalid categories or bad data type formatting.</li><li><strong>Contextual Rules</strong>: Grounding the model with real-world expectations (e.g., a dentist belonging in Healthcare, not Entertainment) helped surface edge cases that might otherwise slip through (see Validation Rules #7 below).</li></ul><pre>%sql<br>SELECT<br>  ai_query(<br>    &quot;databricks-meta-llama-3-3-70b-instruct&quot;,<br>    CONCAT(<br>      &quot;You are a validator for structured US job metadata. Input is one record with fields in this order, pipe-delimited: <br>      <br>      Job Title | Industry | Job Status | AI Impact Level | Median Salary | Experience Years | Required Education. <br>      <br>      Return exactly one character: &#39;1&#39; if the record is plausible and consistent, or &#39;0&#39; if it is not. No explanations. No spaces. No extra text. One character only.<br>      <br>      Validation rules (if any fail, return &#39;0&#39;):<br>        1) Industry must be in Retail, Education, Manufacturing, IT, Finance, Transportation, Entertainment, or Healthcare. <br>        2) Job Status must be in Increasing or Decreasing. <br>        3) AI Impact Level must be in High, Medium, or Low. <br>        4) Median Salary must be an integer only and directionally plausible for the job. <br>        5) Experience Years must be an integer only.<br>        6) Required Education must be in High School, Associate, Bachelor Degree, Master Degree, or PhD. <br>        7) Context checks: <br>          a) Education cannot undershoot obvious professional requirements. Examples: <br>            - Dentist, Physician, Pharmacist, Psychologist require at least Master Degree or PhD. <br>            - Lawyer requires at least Bachelor Degree. <br>            - Registered Nurse requires at least Associate. <br>            - Engineer, Accountant, Auditor require at least Bachelor Degree. <br>          b) Industry must be a reasonable fit for the job. Example: Dentist must be in Healthcare, not Entertainment. <br>          c) Salary must be plausible for the seniority and education level. Example: Economist with Associate and salary over 100000 is implausible. <br>          d) If Required Education is Associate and Median Salary is greater than 100000, return &#39;0&#39; unless the job is a known high-wage exception such as Pilot, Air Traffic Controller, Elevator Mechanic, Power Lineworker, or Radiation Therapist. <br>          e) Senior titles (Senior, Lead, Principal, Director, VP, Chief) normally require at least 3 years of experience. → Junior or Intern titles normally require 0 to 2 years of experience and salaries should not be executive-level. <br>          f) No field may be empty or null. <br>          g) If the job title is extremely ambiguous or contradicts other fields, return &#39;0&#39;. → If all rules pass, return &#39;1&#39;. <br>          <br>      Record: &quot;,<br>      CAST(job AS STRING), &quot;|&quot;,<br>      CAST(industry AS STRING), &quot;|&quot;,<br>      CAST(job_status AS STRING), &quot;|&quot;,<br>      CAST(ai_impact_level AS STRING), &quot;|&quot;,<br>      CAST(median_salary AS STRING), &quot;|&quot;,<br>      CAST(experience_required AS STRING), &quot;|&quot;,<br>      CAST(required_education AS STRING)<br>    )<br>  ) AS qa_flag,<br>  *<br>FROM improved_df</pre><blockquote>It’s worth noting that I iterated quite a bit on the two prompts above. I actually asked ChatGPT to act as an expert prompt engineer, reviewing my “junior” prompts and suggesting improvements. That feedback loop made these prompts significantly more robust.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*F8KYG5jjRFxazSdtOydacg.png" /><figcaption>Validation prompt and preview of output.</figcaption></figure><p>The outcome of this QA prompt: 94% of the rows passed our test. It’s not a perfect 100%, but from a practical standpoint, I wouldn’t trust a human to be 100% accurate in labeling hundreds of jobs either.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/510/1*o1-P6Qgs2S-oz4702LiEDQ.png" /><figcaption>Of the 639 job titles, our second agent labelled 94% as fully accurate.</figcaption></figure><p>I took the ~5% of jobs that failed our QA test and filtered them out in Tableau. The remaining job records felt much more trustworthy and ready to be visualized.</p><p>I aggregated the jobs by industry and AI impact level (low/medium/high). Now I had a clear insight that felt grounded, rather than questionable.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*s4gpt6lriE5HOef2f8EHUg.png" /><figcaption><a href="https://public.tableau.com/app/profile/blake.feiza/viz/AIImpactonDifferentIndustriesMakeoverMonday/MakeoverMonday">Final Visualization Shared on Tableau Public</a></figcaption></figure><h4>Wrapping Up</h4><p>This Makeover Monday dataset wasn’t what I expected, but I’m happy that it pushed me into a new workflow. <a href="https://community.databricks.com/">Databricks Community</a> offered me a way to both reassemble a messy dataset and rethink my approach to validation.</p><p>There are plenty of ways to iterate and refine this process, but I found it encouraging that AI can serve as a builder and validator inside a single notebook. I’d love to hear how you would push this further. What would you try? What would you change?</p><p>Thank you for reading, I’ll see you back here next month with another project!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e7d440813c18" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dataai/making-over-makeover-monday-e7d440813c18">Making Over “Makeover Monday”</a> was originally published in <a href="https://medium.com/dataai">DataAI</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Behind the Build: Sports Radial Calendar]]></title>
            <link>https://medium.com/dataai/behind-the-build-sports-radial-calendar-0ba37ee94910?source=rss-5da05b3c7452------2</link>
            <guid isPermaLink="false">https://medium.com/p/0ba37ee94910</guid>
            <category><![CDATA[tableau]]></category>
            <category><![CDATA[datafam]]></category>
            <category><![CDATA[data-visualization]]></category>
            <category><![CDATA[sports]]></category>
            <category><![CDATA[data-visualisation]]></category>
            <dc:creator><![CDATA[Feiza Brothers]]></dc:creator>
            <pubDate>Mon, 01 Sep 2025 05:01:37 GMT</pubDate>
            <atom:updated>2025-09-01T05:01:37.647Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7KmR9QCP0tyfkQbrysfwow.png" /><figcaption>Behind the Build: 365 Days of Play</figcaption></figure><p>I recently published a Tableau dashboard visualizing the different sports seasons throughout the year. I have always found myself more passionate about data art, and this visualization showcased that. However, I can’t take full credit for the idea and topic. Throughout this post, I’ll walk you through the inspiration for this dashboard, the data preparation process, and the actual build inside Tableau. Hopefully, you’ll learn something applicable to your workflow or be inspired in your own way.</p><p>In this post, I’ve referenced several resources along the way. You can find them all here:</p><ul><li><a href="https://www.tiktok.com/@__sportsball/video/7504296998095490350?_t=ZT-8yZsfHlE9YD&amp;_r=1">SportsBall Inspiration Post</a></li><li><a href="https://public.tableau.com/app/profile/blake.feiza/viz/365DaysofPlay/RadialSportsCalendar">Published Viz on Tableau Public</a></li><li><a href="https://data.world/blakefeiza/365-days-of-play-sports-radial-calendar">Initial Data from ChatGPT and Manual Tweaking</a></li><li><a href="https://data.world/blakefeiza/365-days-of-play-sports-radial-calendar">Prep Flow</a></li><li><a href="https://data.world/blakefeiza/365-days-of-play-sports-radial-calendar">Data Output from Prep Flow</a></li></ul><h3>Source of Inspiration</h3><p>I recently came across an awesome account on TikTok called <a href="https://www.tiktok.com/@__sportsball">SportsBall</a>, which features hand-drawn data visualizations and storytelling. The narration adds an engaging layer to the content as the drawing slowly comes to life. I’ve been obsessed with his content style, and <a href="https://www.tiktok.com/@__sportsball/video/7504296998095490350?_t=ZT-8yZsfHlE9YD&amp;_r=1">this video</a>, in particular, really caught my eye:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/529/1*FwGo7ONQQd5maHXX6OnvXw.png" /><figcaption>Hand-drawn “Seasons Change” Visualization by SportsBall on TikTok</figcaption></figure><p>While I’ve seen (and built) many radial charts in the past, I’d never encountered one that collapses on itself like this one does. Additionally, combining many polygon marks in the middle and lines on the outside makes this visualization a great candidate for map layers. I had to try to build this chart in Tableau.</p><h3>Wrangling &amp; Preparing the Data</h3><h4>Starting with AI</h4><p>I knew from the start that I wanted 365 rows of data (one for each day of the year) and one column per sports league indicating whether the team is in-season, in the playoffs, or off at that time. While I could’ve scavenged the internet to find the dates I needed and built this in Excel, I also love a good generative AI use case. I prompted ChatGPT for the expected start and end dates for the 2025–26 season for the 14 leagues (NFL, NBA, MLB, NHL, MLS, NWSL, WNBA, PWHL, PGA, ATP/WTA, NASCAR, F1, CFB, and NCAAB). I also requested the dates for the golf and tennis majors/championships.</p><p>An unexpected challenge arose with seasons that ran through the end of the calendar year, like the NFL (season runs from September to February). ChatGPT struggled with the continuity between the years because I was trying to return only 365 rows.</p><p>I got 90% of the way there with AI, and the data was small enough that I just made the final adjustments to the records by hand. Once the data was pulled into a table, I was ready to pull it into Tableau Prep to continue this pre-Tableau data journey.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XJ3SYzh2ho-P4CWhSJy1zA.png" /><figcaption>Data in Excel after ChatGPT and manual adjustments</figcaption></figure><h4>Tableau Prep</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5zXEEl4EGZHVaNekrhP34w.png" /><figcaption>Prep Flow with reference numbers described below</figcaption></figure><p>I’ll spare you from walking through every step of the flow, but you can find the whole flow, ready to download, <a href="https://data.world/blakefeiza/365-days-of-play-sports-radial-calendar">here</a>. In a nutshell, these were the transformation steps I performed in Prep to finalize the data construction:</p><p><strong>1.</strong> Pivoted all the leagues to one column called “League”.</p><p><strong>2.</strong> Based on the date column in the CSV, a “Day of Year” calculation (numbers 1–365) and an “Angle” calculation based on the day of the year were created to help with the trigonometry in Tableau.</p><p><strong>3.</strong> To indicate the start and end of every season, especially when the season runs through December 31, a series of calculations/steps was required:</p><ul><li>Using <em>DATEADD()</em> functions, the previous value and upcoming value for every date per league were calculated.</li><li>Based on these previous and upcoming values, created LOD calculations to indicate the start date and end date of every league (e.g., the start date for a league should have a current value of “Regular” and a previous value of “Off”; the end date for a league should have a current value of “Regular” or “Playoffs” and a next value of “Off”).</li></ul><p><strong>4.</strong> Based on every league’s start/end dates, created a Point ID column, where one starts on the first day of the season.</p><p><strong>5.</strong> The target visualization requires polygon marks rather than lines for the five primary leagues in the middle (NFL, NBA, MLB, NHL, MLS). Therefore, I needed to densify my data for these leagues by two so we have enough points to complete the whole polygon (outside layer of points and inside layer of points).</p><h3>Building in Tableau</h3><p>Heads up, as we get into this section, if you’re trying to replicate this process yourself, I’d recommend downloading the workbook from Tableau Public and dissecting it for your use case. I’ll explain the calculations conceptually here and provide a few examples to model afterward. Still, nearly 60 map layer calculations in the sheet would be very messy to copy/paste here.</p><h4>The “Inner Ring” Polygons</h4><p>Starting with the regular season, we have five <em>“&lt;League&gt; Makepoint”</em> calculations to build the lighter-colored polygons in the middle. I’ll use the NBA league calculation as an example:</p><pre>// NBA Makepoint<br>IF <br>    [League] = &#39;NBA&#39; AND <br>    NOT ISNULL([Point ID])<br>THEN<br>    IF [Point ID] &gt; [Max Point ID per League] <br>    THEN<br>        MAKEPOINT(<br>        SIN([Angle]) * (4-[NFL Offseason Deduction]+[pInnerPadding]),<br>        COS([Angle]) * (4-[NFL Offseason Deduction]+[pInnerPadding])<br>        )<br>    ELSE <br>        MAKEPOINT(<br>        SIN([Angle]) * (5-[NFL Offseason Deduction]),<br>        COS([Angle]) * (5-[NFL Offseason Deduction])<br>        )<br>    END<br>END</pre><p>Conceptually, here’s what this calculation is doing:</p><ul><li>Filtering to only the NBA league and removing any null Point IDs (representing the off-season).</li><li>Because a polygon needs points on the shape&#39;s inner and outer edges, we use <em>[Max Point ID per League]</em> to determine when we switch from the outer edge to the inner edge. If you squint, everything else is just <em>MAKEPOINT( SIN(Angle), COS(Angle) )</em>, multiplied by a scalar to determine the radius.</li><li>For the radius value, I’m using a parameter for padding and subtracting a radius value of 1 if a more central sport is in the off-season. This creates the collapsing effect. Since the only sport more central than the NBA is the NFL, I’m only subtracting one off-season deduction. You would see an NFL and NBA subtraction if this were the MLB. Each off-season deduction looks like:</li></ul><pre>// NFL Offseason Deduction<br>{ FIXED [DayofYear]: MAX(<br>INT([League] = &#39;NFL&#39; AND [Value] = &#39;Off&#39;))}</pre><p>…giving a value of 1 or 0 that we deduct from the radius.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/862/1*Y8wDWWNcNllujYepvIXcnA.png" /><figcaption>Adding the five inner ring map layers for regular seasons</figcaption></figure><p>There are also five <em>“&lt;League&gt; Playoffs Makepoint”</em> calculations. These will follow the same behavior as the previous calculations, with one additional filter: <em>Value = “Playoffs”:</em></p><pre>// NBA Playoffs Makepoint<br>IF <br>    [League] = &#39;NBA&#39; AND <br>    NOT ISNULL([Point ID]) AND<br>    [Value] = &#39;Playoffs&#39;<br>THEN<br>    IF [Point ID] &gt; [Max Point ID per League] <br>    THEN<br>        MAKEPOINT(<br>        SIN([Angle]) * (4-[NFL Offseason Deduction]+[pInnerPadding]),<br>        COS([Angle]) * (4-[NFL Offseason Deduction]+[pInnerPadding])<br>        )<br>    ELSE <br>        MAKEPOINT(<br>        SIN([Angle]) * (5-[NFL Offseason Deduction]),<br>        COS([Angle]) * (5-[NFL Offseason Deduction])<br>        )<br>    END<br>END</pre><p>Add them to the sheet independently as new map layers for all ten of these calculations. Change the mark type to Polygon, and add Point ID to Path.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/866/1*U72w6BXSZnFc4o_hZS5eaQ.png" /><figcaption>Adding the five inner ring map layers for playoffs (darker color)</figcaption></figure><h4>The “Outer Ring” Lines</h4><p>We follow a similar pattern for each of the women’s leagues, golf, tennis, racing, and college sports, but the calculations are a bit simpler since we aren’t making polygons; just single lines. As an example, here is the calculation for the National Women’s Soccer League Makepoint:</p><pre>// NWSL Makepoint<br>IF [League] = &#39;NWSL&#39; AND NOT ISNULL([Point ID])<br>THEN<br>MAKEPOINT(<br>9 * SIN([Angle]),<br>9 * COS([Angle])<br>)<br>END</pre><p>Conceptually, we are filtering to the correct league and non-null Point IDs. Then, we create our <em>MAKEPOINT() </em>calculation with <em>SIN/COS</em> of an angle, multiplied by a radius.</p><p>The pattern continues with the playoffs <em>MAKEPOINT()</em>, again adding a filter; <em>Value = “Playoffs”</em>:</p><pre>// NWSL Playoffs Makepoint<br>IF [League] = &#39;NWSL&#39; AND NOT ISNULL([Point ID]) AND [Value] = &#39;Playoffs&#39;<br>THEN<br>MAKEPOINT(<br>9 * SIN([Angle]),<br>9 * COS([Angle])<br>)<br>END</pre><p>The map layers for these 18 calculated fields are each line mark type, with Point ID on path. I adjusted the size of the playoff lines to be slightly larger than the regular season.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*35Z9qMsJPiVydN0SsAQ_1g.png" /><figcaption>Adding the outer line map layers for the regular season and playoffs</figcaption></figure><h4>Dots for Majors and Championships</h4><p>I opted to make a unique calculation for every point on the golf and tennis rings, mainly because there are only eight that I’m including, and the calculation volume was manageable. Here’s an example of what these calculations look like for the Masters beginning:</p><pre>// (4/10) The Masters Begins Makepoint<br>IF [League] = &#39;PGA&#39; AND [Date] = DATE(&#39;2025-04-10&#39;)<br>THEN<br>MAKEPOINT(<br>10.5 * SIN([Angle]),<br>10.5 * COS([Angle])<br>)<br>END</pre><p>For these map layers, the mark type is just a circle, and no path shelf is required.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1013/1*9vMN8xUzdC3ZioERWkf_OA.png" /><figcaption>Adding the eight map layers for the green golf/tennis dots</figcaption></figure><h4>Month Tick Lines &amp; Labels</h4><p>To create the dashed lines running from the circle&#39;s center to the outside, I use <em>MAKEPOINT()</em> calculations with two Point ID values, 1 and 2 (the start and the end of each line). For February, the calculation looks like this:</p><pre>// 02. Feb Tick Mark<br>// Inside of RADIANS(): <br>// [Days before 1st of the Month] / [Days in a Year] * [Degrees in a Circle]<br>MAKEPOINT(<br>CASE [Point ID]<br>WHEN 1 THEN SIN(RADIANS(31/365*360)) * 2<br>WHEN 2 THEN SIN(RADIANS(31/365*360)) * 14<br>END<br>,<br>CASE [Point ID]<br>WHEN 1 THEN COS(RADIANS(31/365*360)) * 2<br>WHEN 2 THEN COS(RADIANS(31/365*360)) * 14<br>END<br>)</pre><p>As noted in the calculation documentation, we are calculating the angle in degrees for the line based on the number of days into the year that we are plotting. That degree value is then converted to radians and used in our <em>SIN/COS</em> functions. With some testing, I decided that radius values of 2 and 14 gave me the line length I sought.</p><p>This took a bit of fiddling for the line labels in the center. I finally aligned on adding half the number of days into each month by the day of the year that the month starts. The radius that felt right for this label was 2.7.</p><p>Here’s an example for February, where 31 days have passed in the year by February 1st, and 59 days will mark the end of February:</p><pre>// 02. Feb Label<br>MAKEPOINT(<br>SIN(RADIANS(<br>    (31-((31-59)/2))/365*360<br>    )) * 2.7<br>,<br>COS(RADIANS(<br>    (31-((31-59)/2))/365*360<br>    )) * 2.7<br>)</pre><p>I add these map layers as a shape (default), lower the opacity to 0%, and manually type a single letter representing the month abbreviation.</p><p>With all of that, and quite a bit of formatting (left as an exercise for the reader), we end up with the final product for the visualization all in a single Tableau worksheet:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xtxppl-vxgkfKcg2pEd9hg.png" /><figcaption>Final Tableau worksheet with all map layers</figcaption></figure><h3>Wrapping Things Up with Figma</h3><p>Other than some quick sheets to create custom legends (just square marks and League groups on the Rows shelf), the only thing left to build was a background image in Figma.</p><p>Based on some inspiration from Nike’s motivational ad campaigns, I opted for a greyscale background with white text to maximize the color contrast for the chart elements.</p><p>I found a copyright-free image of a chain-link fence, removed the saturation, and added some layer blur to reduce distraction from the chart. I added a dark circle behind the chart to increase the contrast further. Besides the image, title text, and dark circle, every element on the dashboard was created natively in Tableau.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HdGJXNmrYJFlRH_Lg5JIjg.png" /><figcaption>Figma background image for dashboard</figcaption></figure><p>Importing this image, adding some text objects, and including our charts gives us our final product:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xSX7MkDOWPxN643EMERTCQ.png" /><figcaption>Image of final dashboard</figcaption></figure><p>In all honesty, this was the most fun project that I saw through from start to finish. Is it perfect? Nope. Does it represent what I saw in my head before I started? Yes, it does.</p><p>If you made it this far, I hope you’ve learned something you can apply to your work. Let me know your thoughts on this dashboard, and share feedback on this write-up. Was it useful? Any insight helps inform what I’ll write about next. Until then, Happy Vizzing! 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0ba37ee94910" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dataai/behind-the-build-sports-radial-calendar-0ba37ee94910">Behind the Build: Sports Radial Calendar</a> was originally published in <a href="https://medium.com/dataai">DataAI</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Tableau Tool-“Tips”]]></title>
            <link>https://medium.com/dataai/tableau-tool-tips-a38aee7d3a44?source=rss-5da05b3c7452------2</link>
            <guid isPermaLink="false">https://medium.com/p/a38aee7d3a44</guid>
            <category><![CDATA[data-visualization]]></category>
            <category><![CDATA[ux-design]]></category>
            <category><![CDATA[data-visualisation]]></category>
            <category><![CDATA[tableau]]></category>
            <category><![CDATA[datafam]]></category>
            <dc:creator><![CDATA[Feiza Brothers]]></dc:creator>
            <pubDate>Fri, 01 Aug 2025 05:02:41 GMT</pubDate>
            <atom:updated>2025-08-01T05:02:41.043Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9nu8CFN3AgUtLjt_tYwKXw.png" /><figcaption>Tableau Tool-“Tips” (#feizadata)</figcaption></figure><p>For any Tableau developer, tooltips are one of the easiest features to forget about when building a dashboard. For many dashboard users, tooltips are one of the first forms of interactivity discovered. A bit ironic, no? Tooltips that provide a good UX are effortless to use, but getting there requires intention from the developer.</p><p>In this post, I’ve compiled five core tips to help you enhance your Tableau dashboards with effective tooltips and keep users coming back.</p><h3>#1: Use Tooltips with Intention, or Don’t Use Them at All</h3><h4>What Good Looks Like</h4><p>While researching this blog post, I came across <a href="https://interworks.com/blog/2023/06/02/say-less-how-to-ensure-your-tooltips-add-value/">Beth Kairys’ post</a> describing what makes a good tooltip. Her perspective resonated with me, so I will echo the same advice here.</p><p>A good tooltip should accomplish at least one of three goals for the user:</p><ul><li>Provide Clarity</li><li>Offer Additional Context</li><li>Invite Additional Exploration</li></ul><p><strong>Tooltips that provide clarity </strong>give the user additional information <strong>beyond the dashboard</strong> itself. Examples include information about the data (source, disclaimers, notes), instructions on interpreting the chart, or citing the inspiration for your analysis.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kOvU6DgOaFiQgfPYUdaT-g.png" /><figcaption><a href="https://public.tableau.com/app/profile/shreya.arya/viz/PlayStationTopNGamesB2VBxGamesNightViz/PlaystationGames-TopN">https://public.tableau.com/app/profile/shreya.arya/viz/PlayStationTopNGamesB2VBxGamesNightViz/PlaystationGames-TopN</a></figcaption></figure><blockquote>The above dashboard by Shreya Arya takes top-performing video games and provides additional context about each game through the tooltip.</blockquote><p><strong>Tooltips that offer additional context</strong> supply the user with more data points supporting what is in the view. Examples include further mark context (i.e., rank, percent change, percent of total) or introducing more granularity (i.e., drill-down, percentage breakout, different visuals to further understanding).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/873/1*yf726MyLRiFxMRqJjHapKA.png" /><figcaption><a href="https://public.tableau.com/app/profile/chimdi.nwosu/viz/MakeoverMonday2024Week31-TheWorldsMostInnovativeCompanies/Dashboard1">https://public.tableau.com/app/profile/chimdi.nwosu/viz/MakeoverMonday2024Week31-TheWorldsMostInnovativeCompanies/Dashboard1</a></figcaption></figure><blockquote>This tooltip in a dashboard by Chimdi Nwosu provides additional data points like rank and growth trends that are not initially accessible in the dashboard.</blockquote><p><strong>Tooltips that invite additional exploration</strong> offer the user ways to go <strong>beyond the dashboard</strong>. The tooltip may yield an option to navigate to another dashboard or leverage a URL action, taking the user to the data source or reference to learn more.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N4W4xeE3km4slRUkhjEYng.png" /><figcaption><a href="https://public.tableau.com/app/profile/john.johansson/viz/TheGoodTheBadAndTheUglyWesternMovieCollection/GBUWestern">https://public.tableau.com/app/profile/john.johansson/viz/TheGoodTheBadAndTheUglyWesternMovieCollection/GBUWestern</a></figcaption></figure><blockquote>At the bottom of John Johansson’s tooltip, a URL action is provided to navigate to the data source for any user seeking additional information.</blockquote><h4>Hiding the Junk</h4><p>After discussing the intent behind an effective tooltip, it’s time to air the dirty laundry — there are way too many tooltips that fail to accomplish anything (I’m guilty of this myself). However, I feel this is less of the developer&#39;s fault and more of a result of Tableau’s default behavior with tooltips. There are two simple ways to add quick polish to a dashboard once it’s been built.</p><p><strong>Uncheck “Include in Tooltip” on the Fields you want to hide</strong></p><p>By default, Tableau will add the data for every pill you add to your sheet in your tooltip. While this might be helpful to the developer when building the dashboard, there are a lot of techniques used under the hood of Tableau that a user does not care about. We can right-click these pills and uncheck “Include in Tooltip”. The row will disappear from the tooltip.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*etJS9IUJtdRdbgI9Kk6C1Q.gif" /><figcaption>Unchecking “Include in Tooltip” to remove a field from the tooltip</figcaption></figure><blockquote>Note that this technique only works if you haven’t manually edited your tooltip. If you’d like to revert, enter the tooltip box and click the “Reset” button in the bottom left. I always recommend starting with this technique and polishing the tooltip manually at the end.</blockquote><p><strong>Disable tooltips on sheets where there is no additional context to reveal</strong></p><p>Going back to our best practices, there will usually be some sheets on a dashboard that fail to accomplish any of the objectives mentioned above (BAN sheets can fall victim to this). This is one of the first things I look for when reviewing a dashboard. Don’t overthink it, turn off the tooltips for these sheets. In the Tooltip card, you can either uncheck the “Show tooltips” option or delete all the text inside the textbox.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/925/1*frbjgkUHpF0Zx388j0zgjg.gif" /><figcaption>Two ways to remove the tooltip from a sheet</figcaption></figure><h3>#2: Formatting Your Tooltip in the Edit Menu</h3><p>Over the last couple years, I’ve gradually learned how to format the text and components inside a tooltip’s edit menu. Here is a summary of the most important takeaways.</p><h4>Use Tabs Instead of Spaces to Align Text</h4><p>By breaking text with a tab instead of a space, you can align everything to the right of the tab across lines. This is how Tableau’s default tooltip behaves, and is why we see all our values aligned to the right of the field names.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mtZGfibvNDDO1_xCj7mlVw.png" /><figcaption>Visual depiction of what a Tab separator does in a tooltip</figcaption></figure><p>You can add more than one tab as well, increasing the spacing between the end of the text on the left and the start on the right. Use the alignment ruler at the top of the tooltip textbox if you can’t get the spacing right with one or many tabs. Moving this ruler left or right will increase or decrease each tab&#39;s size.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8Rp18VVm6_NZSqaykdORIg.png" /><figcaption>Visual depiction of one tab before the &lt;field&gt; vs. two tabs</figcaption></figure><h4>Responsive Tooltips vs. “On Hover”</h4><p>By default, Tableau will make all tooltips responsive. This means the end user will see a tooltip when their mouse lands on top of a mark. This can provide a clean, responsive UX, assuming there aren’t too many marks in the view. With a more computationally intense dashboard, this responsiveness can cause lag for the user.</p><p>Changing the drop-down to “On Hover” forces the user to be slightly more intentional when a tooltip is desired, but saves their CPU from over-exertion.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xnU1iMRwDyVvDSAdilh0Kg.gif" /><figcaption>“Responsive” tooltips vs “On Hover” tooltips</figcaption></figure><h4>Include Command Buttons</h4><p>Another default setting enabled by Tableau is “Include command buttons.” This feature allows the user to manually adjust the number of marks in the view (keep only/exclude values). While sometimes a helpful tool, this poses a significant risk for users who do not understand what this feature does. In the GIF below, observe how we can change the apparent story for 2021 Q4 by removing the marks for Furniture and Office Supplies.</p><p>Also bundled into this setting is the ability for the end user to view the sheet’s underlying data. While sometimes helpful for a quick, ad-hoc data pull, this approach will often surface all the fields in the sheet’s backend (including ones that might not be intended for your user).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ypYuUgVmLUky5WipeUA_5g.gif" /><figcaption>Risks associated with “include command buttons”</figcaption></figure><h4>Allow Selection by Category</h4><p>I’m pretty confident that 90% of people do not know this setting exists. Did you know that if you select a mark on your sheet and click on a dimensional value, you can select all the marks from the same category? This is also a toggle in the tooltip menu that we can deselect. See the GIF below for a visual example.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xvnXTc3kxaoTLuzk-hUnSw.gif" /><figcaption>Functionality enabled with “allow selection by category” checked</figcaption></figure><h3>#3: Conditional Formatting in a Tooltip</h3><p>Have you ever tried to color-encode a value in a sheet’s tooltip? I bet you’ve encountered some difficulties configuring the conditional formatting if you have. Luckily, there is an approach to solve this problem using a couple of calculated fields.</p><p>For example, I’d like to display profit for a given quarter/category in my tooltip. I want the value to be blue if the profit exceeds zero. If the value is negative (or zero), I’d like the value to be red. We can create the following calculations:</p><pre>// Tooltip: Profit is Positive<br>IIF(SUM([Profit]) &gt; 0, SUM([Profit]), NULL)<br><br>// Tooltip: Profit is Negative<br>IIF(SUM([Profit]) &lt;= 0, SUM([Profit]), NULL)</pre><p>These two calculated fields will never display SUM(Profit) simultaneously, so we can add them both to the tooltip and color them both independently.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9hdfvLlOfsSpTqi1H9dqkw.gif" /><figcaption>Applying conditional formatting in the tooltip using the above calculations</figcaption></figure><blockquote>This approach can benefit year-over-year calculations and increase complexity by adding more calculated fields.</blockquote><h3>#4: Understanding Viz-in-Tooltips</h3><p>Viz-in-tooltips (VIT) allow developers to embed another workbook sheet into the tooltip. This feature can be a great way to offer additional context to the chart or drill-down capabilities to benefit the user’s dashboard experience.</p><h4>Decomposing a Viz-in-Tooltip (VIT)</h4><p>Four components combine to create a viz-in-tooltip: <strong>Sheet Name</strong>, <strong>Max Height</strong>, <strong>Max Width</strong>, and <strong>Filter.</strong></p><ul><li>The <strong>Sheet Name</strong> tells Tableau which sheet to render inside your tooltip.</li><li>If you want a <strong>taller chart</strong>, reduce your Max Width or increase your Max Height.</li><li>If you want a <strong>wider chart</strong>, increase your Max Width or reduce your Max Height.</li><li><strong>Filter defaults to “All Fields, &quot; </strong>meaning every shared dimension between the primary and tooltip sheets.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/779/1*egEeJCp1yglfv_eHTyDrfQ.png" /><figcaption>Example viz-in-tooltip syntax</figcaption></figure><h4>Viz-in-Tooltip Example: Bar Charts Inside Line Chart Tooltip</h4><p>I’ve created two charts inside my Tableau workbook: one line chart showing quarterly sales by category, and one bar chart showing quarterly sales by category.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_XyOkzorz09SEuibHNMvUA.png" /><figcaption>Two charts used for the VIT example</figcaption></figure><p>On the first sheet, edit the tooltip. Using the toolbar, select <strong>Insert → Sheets → Sheet 2</strong>. By default, our tooltip will show a single bar (for the quarter and category we’re hovering over).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/420/1*kQHZBc1Gs-sN_odHGmmPhg.png" /><figcaption>Initial VIT, only showing one bar.</figcaption></figure><p>To show all category bars for the mark’s Quarter, we need to edit the Filter. Since we don’t want to filter to the Category (only the Quarter), we can replace &lt;All Fields&gt; with &lt;Quarter&gt;. Now we see three bars in the tooltip, one for each Category for the Quarter we’re looking at.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hAAnBKCeLNeIevSHJ79KEQ.png" /><figcaption>The VIT shows all three category bars by changing our filter from &lt;All Fields&gt; to &lt;Quarters&gt;.</figcaption></figure><p>As mentioned above, viz-in-tooltips can <strong>only filter fields shared between the primary and VIT sheets</strong>. Observe how the tooltip behaves if we add Segment to our VIT sheet. Even with our filter set to &lt;All Fields&gt;, we still see all three segments in the tooltip because it is not a shared field (so Tableau cannot filter on it).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/634/1*wVesAcyOqrvy8xGHZUSbZg.png" /><figcaption>VIT with &lt;All Filters&gt; applied; Segment is only on the VIT sheet, so it doesn’t get filtered</figcaption></figure><h4>Adding Images to Tooltips</h4><p>If you can build it on a sheet, you can put it in a tooltip — that includes images too! I first learned this technique using custom shapes from my Tableau repository, but that requires a little more prep than this approach, I learned from Jeff Shaffer: background maps!</p><ul><li>Add 1 to Columns and 1 to Dimensions</li><li>Turn your mark opacity to 0%</li><li>Go to Map &gt; Background Images &gt; Select your Data Source.</li><li>Select “Add Image”, find your image, and specify X and Y Ranges (both 0 to 2)</li><li>On your sheet, uncheck the show header for your axes and turn off the axis rulers.</li></ul><p>Add this sheet to your tooltip and configure your max height/width — pretty cool!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PdEh5mnwls2kNhaNLYvUwA.png" /><figcaption>Example of an image in a tooltip (this silly headshot was the only photo available at the time of writing)</figcaption></figure><blockquote>If using Image Role properties to pull in an image from a URL path, you can configure the sheet like you would a bar chart. The viz-in-tooltip filters will allow you to pull a single image in from a sheet with all the images.</blockquote><h4>Creating Divider Lines in Tooltips</h4><p>This is another tip I inherited from Jeff Shaffer. We can also use a VIT to create nice, thick divider lines within a tooltip. Create a new blank sheet, and go to your row divider lines. Customize your divider here.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4j5NGHUH12CvCDEIG0Qs0A.png" /><figcaption>Blank worksheet with formatted row dividers to use in other tooltips</figcaption></figure><p>Now add this sheet to your tooltip with maxheight=9 and voila! You can use the same sheet as often as you’d like by copying/pasting.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/610/1*1Kbat8I1ZCjpp5OaJkdBVQ.png" /><figcaption>Example tooltip with VIT divider lines</figcaption></figure><h3>#5. Editing Tooltips From the Dashboard</h3><p>This final nugget is a small one that I picked up from Lisa Trescott’s presentation at TC24. Since, commonly, tooltips are one of the final touches on a dashboard, I find this tip particularly valuable. Instead of navigating to every sheet one by one to configure your tooltips, you can edit them directly from the dashboard page.</p><p>While on your dashboard, click on the sheet with the tooltip you’d like to edit. Use the top navigation bar to select Worksheet → Tooltip. Surprise! We’re greeted by our friendly edit tooltip menu, available directly from the dashboard screen. 🙂</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TqSWaOk9wAeEOA8jQXTUhw.gif" /><figcaption>Editing a sheet’s tooltip from the dashboard.</figcaption></figure><blockquote>On Windows, a shortcut is Alt + W + O (<strong>W</strong>orksheet → T<strong>O</strong>oltip).</blockquote><h3>Great Additional Resources</h3><p>While we’ve reached the end of the content I gathered for this blog post, there is always more to test and explore with Tableau. Tooltips are no exception. If you’re interested in reading more, here are some great resources I’d recommend checking out:</p><ul><li><a href="https://interworks.com/blog/2020/08/17/going-deep-with-tableau-tooltips-switching-vizzes/">Dynamic Viz-in-Tooltip (Chart Swap) — Angus Eady</a></li><li><a href="https://interworks.com/blog/2020/08/28/going-deep-with-tableau-tooltips-multiple-vizzes/">Organizing Multiple Charts in a Tooltip — Angus Eady</a></li><li><a href="https://interworks.com/blog/2020/09/04/going-deep-with-tableau-tooltips-user-control-for-on-off-switching/">On/Off Tooltip Toggles — Angus Eady</a></li><li><a href="https://interworks.com/blog/2020/09/18/going-deep-with-tableau-tooltips-background-formatting/">Formatting Tooltip Background colors — Angus Eady</a></li><li><a href="https://interworks.com/blog/2023/06/02/say-less-how-to-ensure-your-tooltips-add-value/">How to Ensure Your Tooltip Adds Value — Beth Kairys</a></li><li><a href="https://www.theinformationlab.co.uk/community/blog/tooltips-tableau-dashboards-user-experience-perspective/">Tooltips From a UX Perspective — Tim Ngwena</a></li><li><a href="https://www.dataplusscience.com/TableauTips12.html">10 Tips for Viz-in-Tooltips — Jeff Shaffer</a></li><li><a href="https://www.dataplusscience.com/TableauTips13.html">Another 10 Tips for Viz-in-Tooltips — Jeff Shaffer</a></li></ul><p>As always, thank you for reading this blog post. Leave a comment and a clap if you learned something new (I read them all, and it’s nice to know someone is out there). See you next month! 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a38aee7d3a44" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dataai/tableau-tool-tips-a38aee7d3a44">Tableau Tool-“Tips”</a> was originally published in <a href="https://medium.com/dataai">DataAI</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Tableau Map Layers Magic: Striped Bar Chart]]></title>
            <link>https://medium.com/retail-tableau-user-group/tableau-map-layers-magic-striped-bar-chart-67a4b7f544ea?source=rss-5da05b3c7452------2</link>
            <guid isPermaLink="false">https://medium.com/p/67a4b7f544ea</guid>
            <category><![CDATA[datafam]]></category>
            <category><![CDATA[tableau]]></category>
            <category><![CDATA[data-visualization]]></category>
            <category><![CDATA[tableau-advanced]]></category>
            <category><![CDATA[tableau-public]]></category>
            <dc:creator><![CDATA[Feiza Brothers]]></dc:creator>
            <pubDate>Tue, 01 Jul 2025 05:02:56 GMT</pubDate>
            <atom:updated>2025-10-12T20:25:00.724Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DeHYpnRs2hHz5tQ8WCw_UQ.png" /><figcaption>Tableau Map Layers Magic: Striped Bar Chart</figcaption></figure><h3>Initial Inspiration</h3><p>For a recent Makeover Monday challenge, I used map layers to create stripes on my bar chart to represent battery charge for a work energy-related dataset.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OfPn9yIMMoo6oKEWQG4atw.png" /><figcaption><a href="https://public.tableau.com/app/profile/blake.feiza/viz/WorkProductivityBenchmarksHourlyAllocationsMakeoverMonday/WorkProductivityBenchmarks">https://public.tableau.com/app/profile/blake.feiza/viz/WorkProductivityBenchmarksHourlyAllocationsMakeoverMonday/WorkProductivityBenchmarks</a></figcaption></figure><p>With several people interested in using the technique themselves, I decided it would be worth creating a tutorial for the approach. To keep this tutorial easy to follow, I’ll be using the Superstore dataset instead.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mDnQ-UhJ42tZFyB0ea-wLA.png" /><figcaption>Final Viz from this Tutorial (<a href="https://public.tableau.com/app/profile/blake.feiza/viz/StripedBarsTutorialwithMapLayersFeizaData/MapLayersStripedBarsDashboard">https://public.tableau.com/app/profile/blake.feiza/viz/StripedBarsTutorialwithMapLayersFeizaData/MapLayersStripedBarsDashboard</a>)</figcaption></figure><h3>Calculations Used</h3><p>For copy/paste convenience, I’ll list all of the calculations here before explaining their purpose and applications below:</p><pre>// Normalized Sales per Region<br>{ FIXED [Region]: SUM([Sales])}<br> / <br>{ MAX({ FIXED [Region]: SUM([Sales]) }) }<br><br><br>// Total Sales Bars<br>MAKEPOINT(<br>CASE [Point ID]<br>WHEN 1 THEN 0<br>WHEN 2 THEN 1<br>WHEN 3 THEN 1<br>WHEN 4 THEN 0<br>WHEN 5 THEN 0<br>END<br>,<br>CASE [Point ID]<br>WHEN 1 THEN 0<br>WHEN 2 THEN 0<br>WHEN 3 THEN [Normalized Sales per Region]<br>WHEN 4 THEN [Normalized Sales per Region]<br>WHEN 5 THEN 0<br>END<br>)<br><br><br>// Dummy Map Layer<br>MAKEPOINT(0,0)<br><br><br>// Y-Value<br>// Using Modulo functions to handle all 4 corners of each stripe<br>IF [Point ID] % 4 = 2<br>THEN 1<br>ELSEIF [Point ID] % 4 = 3<br>THEN 1<br>ELSEIF [Point ID] % 4 = 1<br>THEN 0<br>ELSEIF [Point ID] % 4 = 0<br>THEN 0<br>END<br><br><br>// X-Value<br>// Using Modulo functions to handle all 4 corners of each stripe<br>IF [Point ID] = 1<br>THEN 0<br>ELSEIF [Point ID] &gt; [pSlashes]*4<br>THEN NULL<br>ELSEIF ([Point ID] - 2) % 4 = 0<br>THEN (2 * FLOOR(([Point ID] - 2) / 4) + 1)/[pSlashes]/2<br>ELSEIF ([Point ID] - 2) % 4 = 2<br>THEN (2 * FLOOR(([Point ID] - 2) / 4) + 1)/[pSlashes]/2<br>ELSEIF ([Point ID] - 2) % 4 = 1<br>THEN (2 * FLOOR(([Point ID] - 2) / 4) + 2)/[pSlashes]/2<br>ELSEIF ([Point ID] - 2) % 4 = 3<br>THEN (2 * FLOOR(([Point ID] - 2) / 4) + 2)/[pSlashes]/2<br>END<br><br><br>// Striped Bars<br>MAKEPOINT([Y-Value], [X-Value])<br><br><br>// Bar Label<br>MAKEPOINT(0.5, [Normalized Sales per Region])</pre><p>Without further ado, let’s begin!</p><h3>Densify your Data</h3><p>As with most map layers techniques, we start by densifying our data. I will join to a dataset with a single column, [Point ID], with values ranging from 1 to 1000. You can create this single-column dataset in Excel yourself, or download it <a href="https://data.world/blakefeiza/1000-points-data-for-map-layers-densification">here</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/825/1*kw5khzynqYzHQZEqOgiEKA.png" /><figcaption>Select “Create Relationship Calculation” and Input 1 on Both Sides.</figcaption></figure><p>By creating a relationship calculation for both sides of the equation, I input 1 into both sides (1=1). Since 1 will always equal 1, Tableau is able to connect all 1000 points to every row in the Superstore dataset. This will be useful when we build our map layers with points and coordinates.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/859/1*iEE79axlhoXL6GL8bIHA2Q.png" /><figcaption>After creating the 1=1 Relationship Calculation.</figcaption></figure><h3>Creating the Total Sales Bars</h3><h4>Calculation: Normalized Sales per Region</h4><p>The first step will be to normalize our sales value for every region, to get values between 0 and 1 for each region’s sales.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/788/1*wjc3zimAuUGtSlJLDWsqog.png" /><figcaption>Normalized Sales per Region Calculation.</figcaption></figure><p>This normalization step removes any issues related to the max and min values on a map for latitude and longitude. It also makes subsequent calculations significantly easier when adding the striped map layer.</p><h4>Calculation: Total Sales Bars</h4><p>Now that we have our data densified and our sales normalized, we are ready to start building our map layers. We will use our [Point ID] values (specifically Point IDs 1–5) to create each of our base rectangles. The following diagram should help you understand how we will connect our points to create a rectangle.</p><p>Note that Point IDs 1 and 5 should always have the same position to complete the shape.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/713/1*aBPb4I5vBWwbJ0XA7u7gzw.png" /><figcaption>Visual representation of using Point ID to plot coordinates of a rectangle.</figcaption></figure><p>A less intuitive attribute of the <em>MAKEPOINT()</em> function is that when thinking in an X, Y coordinate plane, we actually list our <em>Y-value first</em>. This is likely because Tableau users are primarily using Latitude and Longitude values in this function, but it always helps me remember this pattern by thinking <strong><em>MAKEPOINT( [Rise], [Run] )</em></strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/791/1*jBREz0etLjfO2HYE-fqLtg.png" /><figcaption>How to interpret the argument order in the MAKEPOINT() function</figcaption></figure><p>With those two diagrams in mind, here is our calculated field for our [Total Sales Bars].</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/789/1*e0pJd2DTy5m-acwyw2JfGw.png" /><figcaption>Total Sales Bars Map Layer Calculation</figcaption></figure><p>Double-click the [Total Sales Bars] calculation to add it to our sheet as our first map layer.</p><ul><li>Change the Mark Type to Line.</li><li>Add [Point ID] to Path.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FQ-iu4guwyrUIKvRz7KK8w.png" /><figcaption>After adding Total Sales Bars to the view.</figcaption></figure><p>Things look weird because we’re on a map, zoomed in really close off the west coast of Africa. In order to move to an X, Y plane, we need at least two map layers in the view. We can add a [Dummy Map Layer] for now.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/838/1*Ipx0zNvQ7JxrPkULd158wA.png" /><figcaption>Dummy Map Layer Calculation.</figcaption></figure><p>Drag [Dummy Map Layer] to the top left corner of the view to add a marks layer, giving us two map layers on the pane.</p><p>Now, go to the top control bar and select <strong><em>Map &gt; Background Maps &gt; None</em></strong>. This gives us our first rectangle, sitting within a coordinate plane in the view.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*V_0kyMFRimiLoeBMpxtVEQ.png" /><figcaption>After adding Dummy Map Layer and Disabling Background Maps</figcaption></figure><p>If we add [Region] to Rows and change our fit from Standard to Entire View, we can start to see the skeleton of our chart.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SyBv79WGmJVxuEZhaPoPdg.png" /><figcaption>After adding Region to Rows and changing fit to Entire View</figcaption></figure><h3>Creating the Striped Map Layer</h3><h4>Parameter: pSlashes</h4><p>Because I enjoy controlling the number of slashes in each bar, I like to create a parameter to gain this control. Whether exposing the parameter to the end user or not, I still find it useful to easily manipulate the look of the bars with a parameter. A good default value for this parameter is 5.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/551/1*zPay_QKhFiyw7-tB2GtKrg.png" /><figcaption>pSlashes Parameter Configuration</figcaption></figure><h4>Calculations: Y-Value, X-Value, and Striped Bars</h4><p>Next up, we will make our [Striped Bars] map layer. Because these calculations can be a bit complicated, I’ve broken them into [X-Value] and [Y-Value] sub-calculations.</p><p>To illustrate the goal, we are using points in groups of 4 to create each individual stripe going through the bar. Once we reach the end of our final stripe (Point 12), we use one more point (13) to take us back to the starting point. If we turn this layer into a Polygon mark type, this will fill in each of the sections to give us the desired “stripe” effect.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kKvjVSQIlusI4ecnF_h5QQ.png" /><figcaption>Visual representation of the Striped Bars Calculated Field.</figcaption></figure><p>To accomplish our goal, we can use the modulo operator (%). Modulo enables us to return the remainder for every value of [Point ID] divided by 4.</p><p>For example,<em> 8 % 4</em> = 0, because 8 divided by 4 returns 2, with a remainder of 0.</p><p>Similarly, <em>9 % 4</em> = 1, because 9 divided by 4 returns 2, with a remainder of 1.</p><p>Combining this operator with the MAKEPOINT() logic above, we have the following calculations:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/794/1*o3cDJaumu5ljDtoOSJOgtA.png" /><figcaption>Values alternate between 0 and 1 on the Y-Axis to span the height of the bars.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/789/1*_Q8PgJp33sqar5LCovwvIg.png" /><figcaption>Values increase gradually as Point IDs increase, moving in a 4-Point pattern.</figcaption></figure><p>We can put these two sub-calculations inside a MAKEPOINT() function to create the final map layer.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/790/1*P7y6fgF6PgoaKttj7_mihw.png" /><figcaption>Combining the Y-Value and X-Value calculations inside MAKEPOINT().</figcaption></figure><p>Now we can add our [Striped Bars] as a Marks Layer.</p><ul><li>Change the Mark Type to Polygon.</li><li>Add [Point ID] to Path.</li></ul><p>We can also remove our [Dummy Map Layer] from the view now that we have 2 other layers added.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hPj3vRfx3T0hoi19ieK8bw.png" /><figcaption>After adding Striped Bars to the view</figcaption></figure><h3>Cleaning Up the View</h3><p>We’re almost there.</p><p>Drag [Total Sales Bars] onto the view <em>again</em> to add a second Marks Layer with the same calculation.</p><ul><li>Change the Mark Type to Polygon.</li><li>Add [Point ID] to Path.</li></ul><p>Re-order the three marks layers so that we have, from top to bottom:</p><ol><li>Total Sales Bars (Line Mark)</li><li>Striped Bars (Polygon Mark)</li><li>Total Sales Bars (Polygon Mark)</li></ol><p>Change the Mark Color of Striped Bars to White.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*j3RvYu2s8y3XNzGrsEf0cA.png" /><figcaption>After adding the outline map layer, reordering, and changing the Stripe color</figcaption></figure><p>For some additional formatting:</p><ul><li>Increase the Mark Size of Total Sales Bars (Line Mark).</li><li>Add [Region] to Color for both our Total Sales Bars layers.</li><li>Edit your [pSlashes] value to control the number of stripes going through the bars.</li><li>Disable Selection in the Marks Card for Total Sales Bars (Line Mark) and Striped Bars.</li><li>Remove grid lines, zero lines, row/column dividers, and axis headers.</li><li>Filter or hide the null indicator in the bottom right.</li><li>Clean up/customize/remove your tooltips on the Total Sales Bars (Polygon Mark).</li><li>Rename your Map Layers to stay organized.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dhIP5fq3vulULWsMoeDNWw.png" /><figcaption>Workbook after formatting steps.</figcaption></figure><h3>Optional: Add a Custom Bar Label</h3><p>If we want to use a custom label at the end of our bars, we can do that with one more calculation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/791/1*SrUWB5IP7b1Ij1czgsSj-Q.png" /><figcaption>Calculation for labels at the end of the bars.</figcaption></figure><p>Add [Bar Label] as a Marks layer.</p><ul><li>Change the Mark Type to Circle.</li><li>Lower Opacity to 0%.</li><li>Disable Selection on the Map Layer.</li><li>Add [Region] and SUM(Sales) to Label.</li><li>Set the Label Alignment to Middle Right (Controls where the text sits relative to the Circle).</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/302/1*Z0QdTCrm7x6d0Q_WP1_WRw.png" /><figcaption>Label Alignment (sets the text to the right of the circle)</figcaption></figure><ul><li>Format the Label text. Left align inside of the label text (Controls how the text sits within the textbox).</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/990/1*6jEtmcB26BFRYzJbm9HB0g.png" /><figcaption>Formatting the label text (left aligning the text on the right side of the circle)</figcaption></figure><ul><li>Unhide the [Longitude] axis and set a fixed axis range to add some space for the labels to breathe.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wgrmnxJBzwhirskFafGLDg.png" /><figcaption>Workbook after adding the custom bar labels</figcaption></figure><p>And, done!</p><h3>Wrapping Things Up</h3><p>Well, this technique is definitely not a “Show Me” bar chart… But hopefully, for some more niche use cases, this tutorial is beneficial for the craziest of the #DataFam.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mDnQ-UhJ42tZFyB0ea-wLA.png" /><figcaption>Final dashboard (<a href="https://public.tableau.com/app/profile/blake.feiza/viz/StripedBarsTutorialwithMapLayersFeizaData/MapLayersStripedBarsDashboard">https://public.tableau.com/app/profile/blake.feiza/viz/StripedBarsTutorialwithMapLayersFeizaData/MapLayersStripedBarsDashboard</a>)</figcaption></figure><p>What did you think about this tutorial? Leave a comment if you found this helpful, or share ideas for future content on this blog. I’ll see you all next month with another #feizadata blog post! 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=67a4b7f544ea" width="1" height="1" alt=""><hr><p><a href="https://medium.com/retail-tableau-user-group/tableau-map-layers-magic-striped-bar-chart-67a4b7f544ea">Tableau Map Layers Magic: Striped Bar Chart</a> was originally published in <a href="https://medium.com/retail-tableau-user-group">Retail Tableau User Group</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>