<?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"><channel><title><![CDATA[Quantitative UX Research Blog]]></title><description><![CDATA[A blog about Quant UX Research, applications, topics, and careers. Features additional discussion related to the book "Quantitative User Experience Research" and the Quant UX Conference.]]></description><link>https://quantuxblog.com</link><generator>RSS for Node</generator><lastBuildDate>Sun, 12 Apr 2026 03:14:58 GMT</lastBuildDate><atom:link href="https://quantuxblog.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[My UX Research "Rolodex" ]]></title><description><![CDATA[I'm often asked to recommend a tool or vendor for a research project. Over the course of 20+ years doing applied research, I've come to rely on a set of suppliers and software to consider when needed ]]></description><link>https://quantuxblog.com/my-ux-research-rolodex</link><guid isPermaLink="true">https://quantuxblog.com/my-ux-research-rolodex</guid><category><![CDATA[#Ux research]]></category><category><![CDATA[survey research]]></category><category><![CDATA[quantux]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Tue, 24 Feb 2026 19:59:05 GMT</pubDate><enclosure url="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/64ee1580b33ffb853a63cb58/a7129942-b823-46c4-9346-f244829a1b10.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm often asked to recommend a tool or vendor for a research project. Over the course of 20+ years doing applied research, I've come to rely on a set of suppliers and software to consider when needed on any project. Here is that list!</p>
<p>First, a few caveats. <strong>No one here knows I'm recommending them</strong>, and I'm not incentivized to do so. Still, if you contact them, it's fine to mention my recommendation (or not). <em>Please don't infer anything from the absence of anyone</em>. It might be that I don't recommend them, but more likely I don't know of them, forgot about them, or the list was too long to include them. Finally, this list reflects my own needs and history. It may not align perfectly with anyone else.</p>
<p>In a nutshell, these are folks I call first. If you know what a Rolodex is, this is mine. Working with these folks will help your projects, too!</p>
<hr />
<h3>General Software Tools</h3>
<p><em>Before recommending suppliers, these are the baseline tools I use everyday.</em></p>
<p><strong>R and RStudio</strong>. This is pretty obvious, especially given my <a href="https://www.amazon.com/Marketing-Research-Analytics-Use/dp/3030143155">R book</a>. I mostly use base R instead of <code>tidyverse</code> for simplicity and long term stability. I have nothing against <a href="https://www.amazon.com/Python-Marketing-Research-Analytics-Schwarz/dp/3030497194">Python</a>, though. My Quant UX book was actually <a href="https://quantuxblog.com/how-the-quant-ux-book-was-written">written inside RStudio</a>, from draft through near-final PDF! BTW, I reported empirical observations of my <a href="https://quantuxblog.com/favorite-r-packages-part-1">most-used R packages in this post</a>.</p>
<p><strong>Google Docs</strong>. Again, pretty obvious. I will call out Docs' speed, reliability, and seamless abilities to collaborate and co-edit. Yes, there are downsides and I wish Proton was a more full-featured alternative but it isn't. (In case you're wondering, no, I do not use Excel or Word.)</p>
<p><strong>TexShop / LaTeX</strong>. The <a href="https://www.amazon.com/Marketing-Research-Analytics-Use/dp/3030143155">R book</a> and the <a href="https://www.amazon.com/Python-Marketing-Research-Analytics-Schwarz/dp/3030497194">Python book</a> were both written start-to-finish using <a href="https://pages.uoregon.edu/koch/texshop/">TeXShop</a>, all the way to a near camera ready PDF handed off to the publisher. The <a href="https://www.quantuxbook.com">Quant UX book</a> was also LaTeX but using RStudio instead of TeXShop. The start-up effort of LaTeX is high but the power is unmatched. I love to see things "camera ready" as I write.</p>
<p><strong>GitHub / BitBucket</strong>. My coauthors and I collaborate using GitHub (or BitBucket, depending) so we can work on drafts simultaneously, and have backups and versioning. If you code, or write using any local file system tool (e.g., Scrivener), you should learn git.</p>
<p><em>BTW, my laptops have all been Macs for the past 15 years. In addition to other features, Macs are especially well-suited for R and LaTeX users. I'm writing this with a MacBook Pro M5.</em></p>
<hr />
<h3>Survey Research Suppliers</h3>
<p><em>Platforms and people I rely on for great survey research authoring and fielding.</em></p>
<p><a href="https://sawtoothsoftware.com/discover"><strong>Sawtooth Discover</strong></a>. This is my all-purpose survey authoring &amp; hosting platform. Sawtooth has long been the leader for MaxDiff and conjoint analysis surveys, but even when I'm just doing a general survey I use their platform. It's free to use for up to N=50 respondents.</p>
<p><a href="https://connect.cloudresearch.com"><strong>Cloud Research</strong></a>. Their Connect product is my most-used provider to obtain general "self service" panel responses to surveys. It is exceptionally easy to use and transparent on pricing, and they have done a lot of work on respondent quality.</p>
<p><a href="https://www.verasight.io"><strong>Verasight</strong></a>. They are a new player in the space of quality sampling and research with probability-based survey panels. I have not used them yet, but they do great research on the current sample landscape, and they are now on my short list for DIY sample needs.</p>
<p><a href="https://answersresearch.com"><strong>Answers Research</strong></a>. For 15+ years, when I've needed someone to manage survey research I can't handle fully — complex fielding, multiple languages, authoring with stakeholders, fielding, qualitative add-ons, and/or analytics — Answers is the first partner I've asked.</p>
<p><a href="http://www.unabashedresearch.com"><strong>Unabashed Research</strong></a>. This is a different kind of recommendation. When I want someone to directly partner with me to scope, lead, run, and report research, my first call is to James Alford at Unabashed. I've worked with him on dozens and dozens of projects for 20 years.</p>
<hr />
<h3>General Research Suppliers</h3>
<p><em>The folks I call when I need someone to lead specific projects, especially in-person research. I have used these groups for many projects, each for 15+ years. Besides the US, I have done a lot of research in Japan; it is an unusually rewarding and insightful locale.</em></p>
<p><a href="https://caelus-usa.com"><strong>Caelus</strong></a> (US). I've known the folks at Caelus for 25 years and they handle everything from in-person research, to validated recruiting, to online survey and related projects. They are unparalleled at assessing research needs and helping to form honest, transparent plans.</p>
<p><a href="https://blinkux.com/services/insights-strategy"><strong>Blink UX</strong></a> (US). Blink UX is a large agency able to handle all aspects of UX product work from research, to concept testing, to design iteration. Several of their leaders are former close colleagues of mine, and I highly recommend them.</p>
<p><a href="https://www.r2-insights.com"><strong>R2 Insights</strong></a> (Japan). A boutique, highly personalized research service provider for Japan. For English-speaking product teams, R2 is the ideal partner to help you understand the Japan market. I've done 20+ projects with them over the years.</p>
<p><a href="https://www.sugataresearch.com"><strong>Sugata Research</strong></a> (Japan). My other favorite Japan research agency is Sugata, with whom I've also done many projects over the years. They are larger than R2 Insights and may be able to support you with a larger team when that is needed (e.g., parallel, multi-city research).</p>
<hr />
<h3>Qualitative Research Facilities</h3>
<p><em>When I run field research myself (as opposed to have one of the above suppliers lead it) — whether it involves in-depth interviews, home visits, or focus groups — I call these folks for recruiting and local facilities. (For Japan, see R2 Insights and Sugata Research above.)</em></p>
<p><a href="https://www.fieldwork.com/market-research-services/research-venues/"><strong>Fieldwork</strong></a> (US). These are top-notch research facilities all around the US. They have layouts suitable for in-depth interviews, focus groups, usability and concept tests, and specialty projects such as cooking tests. Very easy to coordinate efficient, multi-city projects.</p>
<p><a href="https://sago.com/en/"><strong>Sago</strong></a> (Europe). I've used their network and partners many times in Munich, Paris, Berlin, London, Hamburg, and Rome. In the whole world, my personal all-time favorite research facility is their location in old city Munich!</p>
<p><a href="https://www.acornasia.com"><strong>Acorn Asia</strong></a> (Asia Pacific generally). I've used Acorn Asia for multiple projects; if I recall correctly, in China, South Korea, Indonesia, and the Philippines. Similar to Fieldwork and Sago, it is easy to coordinate multiple locations as part of a single efficient project plan.</p>
<hr />
<h3>P.S. A Plug for International Research</h3>
<p>It is shockingly common for US-based companies to skip international customer research, despite the fact that customer needs, perceptions, considerations, and behaviors in other countries may vary substantially from the US.</p>
<p>The usual reasons not to do international research are:</p>
<ul>
<li><p>the US market is the largest</p>
</li>
<li><p>we'll launch first in the US</p>
</li>
<li><p>any other single market is too small</p>
</li>
<li><p>it is too expensive to do international research</p>
</li>
<li><p>we don't have the time for international research</p>
</li>
</ul>
<p>My responses to those claims are:</p>
<ul>
<li><p><em>the US market is the largest</em> ==&gt; OK, and the rest of the world is larger when combined</p>
</li>
<li><p><em>we'll launch first in the US</em> ==&gt; it's better to find international problems before the product is locked-in for any market</p>
</li>
<li><p><em>any other single market is too small</em> ==&gt; the point is not to test or develop for any single market, but to use one or two locations to "pressure test" the product and needs</p>
</li>
<li><p><em>it is too expensive to do international research</em> ==&gt; don't do international testing all the time, but allocate some money; the risk of not doing it is higher than cost of doing some</p>
</li>
<li><p><em>we don't have the time for international research</em> ==&gt; with efficient research partners as above, it will take very little additional time</p>
</li>
</ul>
<p>As a general high-level matter, as a US-based researcher for US-based companies, I tended to allocate around 20% of projects to international locations. That ensured that we understood them at a baseline level, with enough "sample" to be confident we would catch big issues. Yet it was a small enough proportion to ensure that we placated stakeholders about the overall level, and it did not claim excessive time.</p>
<p>A shorter way to explain it: <strong>if you're building for the whole world (and you probably are), then you owe it to the world to consider their needs and issues</strong>. Ultimately, those customers are paying you — and you should devote some research to them!</p>
]]></content:encoded></item><item><title><![CDATA["Rigor" in Quant UX Research]]></title><description><![CDATA[I’ve recently had discussions with other Quant UXRs about “rigor” and what it means in our work. I’ve often heard it raised as a question over the years, and this prompted me to compile some thoughts.
I propose a stack with 4 levels of how I view “ri...]]></description><link>https://quantuxblog.com/rigor-in-quant-ux-research</link><guid isPermaLink="true">https://quantuxblog.com/rigor-in-quant-ux-research</guid><category><![CDATA[quantux]]></category><category><![CDATA[#Ux research]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Tue, 03 Feb 2026 22:50:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/OqClyFZl3Go/upload/42f3014be05edeb2bf6d902ebca571a5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I’ve recently had discussions with other Quant UXRs about “rigor” and what it means in our work. I’ve often heard it raised as a question over the years, and this prompted me to compile some thoughts.</p>
<p>I propose a stack with 4 levels of how I view “rigor” … and first, I’ll start with what is <em>not</em> rigor.</p>
<hr />
<h2 id="heading-what-is-not-rigor">What Is Not Rigor?</h2>
<p><strong>Rigor is not a synonym for advanced methods</strong>. In fact, I believe that using so-called advanced methods (such as, say, causal modeling, deep learning, multilevel regression) is too often a crutch that people grab when they are confronted with a serious problem <em>apart</em> from analytics. A few of those problems are:</p>
<ul>
<li><p><em>Insecurity</em>. They want to use advanced methods to say “look how smart I am”</p>
</li>
<li><p><em>Poor data</em>. They hope to rescue something when the data set is a mess</p>
</li>
<li><p><em>Wanting to learn something</em>. They’d like to stretch the boundaries of their knowledge</p>
</li>
</ul>
<p>I am <em>not</em> saying those problems are bad or wrong to address. Also, I am not saying that every use of an advanced method is wrong. Far from it! Quants should happily use any method that is appropriate. Rather, I am simply observing that complex methods are often chosen for reasons <em>other than</em> statistical suitability. And even when they are suitable, a complex analysis is not inherently more rigorous than a simple one.</p>
<p><strong>Rigor is not statistical significance or p-values</strong>. First of all, as a Bayesian, I don’t find the concepts of <em>statistical significance</em> and <em>p-values</em> to be very useful, for reasons similar to <a target="_blank" href="https://www.fharrell.com/post/journey/">those described by Frank Harrell</a> (who TBH is a much more knowledgable statistician than I am). Setting Bayesian stats aside, even in the frequentist world statistical significance is closely tied to null hypothesis significance testing (NHST) … and in my opinion that’s not what quants should do most of the time (see <a target="_blank" href="https://www.sjsu.edu/faculty/gerstman/misc/Cohen1994.pdf">Cohen’s famous takedown</a> of p-values and NHST).</p>
<p>Instead, I believe quant UXRs should use data to learn and inform decisions. Most of that process cannot be forced into an NHST paradigm, and assigning a p-value to some portion is misleading at best.</p>
<p>In short, statistical significance is a misleading concept. Unfortunately, those who ask about it most often may understand it the least. Again, that’s not 100% of the time and NHST has occasional uses (some forms of A/B and multivariate testing <em>sometimes</em> are exceptions).</p>
<p><strong>Rigor is not massive data</strong>. I often see studies that attempt to collect massive data, either at a sample level (such as 10000s of survey responses, or 100Ms of online users) or at a response level (such as collecting 100s or 1000s of variables per observation). The problem is that massive data is not necessarily good data, and analysis of it may increase the odds of finding spurious associations and misleading results.</p>
<p>In other words, a bigger load of garbage is still a load of garbage, and “advanced methods” (see above) won’t save it. Getting more data is not more “rigorous,” it is just more data, good or bad.</p>
<p><strong>Rigor is not an unwavering protocol</strong>. This is a problem I sometimes see when a researcher transitions from academia to industry. One common form is to put every UX research participant through an identical set of instructions, tasks, and data collection methods. This may occur in interviews, usability tests, or focus groups, as well as surveys. The usual motivation is to collect identical, consistent, and “unbiased” data.</p>
<p>The problem with a rigid protocol is this: it maximizes the defensibility of a data against criticism from other researchers (such as journal reviewers) but it does not maximize what we learn from our participants. I vastly prefer to engage with participants and adapt an interview or other protocol in response to what I hear. If that gives “incomparable data,” then that is <em>my</em> problem to determine how to have impact with stakeholders … but however I resolve that, I will have learned more from participants than I would by subjecting them to a tedious, unwavering test. And that maximizes business value of the research.</p>
<p>In summary, rigorous research does <em>not</em> imply or necessitate:</p>
<ul>
<li><p>Advanced methods</p>
</li>
<li><p>A focus on hypothesis testing, “statistical significance,” or p-values</p>
</li>
<li><p>Massive data sets</p>
</li>
<li><p>Unwavering protocols for interviews or other data collection</p>
</li>
</ul>
<hr />
<h2 id="heading-what-is-rigor">What is Rigor?</h2>
<p>You may have noticed above that I repeatedly suggested that <strong>the point of research is to learn</strong> from our users, customers, and research participants <strong>and to inform product or other business decisions</strong>. To do that <em>rigorously</em> is to learn deeply and to focus relentlessly on making decisions well.</p>
<p>To those ends, I propose the following four area of research rigor. I believe there is some degree of logical order to these areas such that each one presupposes and is built on the previous one, yet we could also view them as complementary rather than strictly nested.</p>
<h3 id="heading-rigor-1-focus-on-learning-and-decisions">Rigor 1. Focus on Learning and Decisions</h3>
<p>The first area of rigor is to focus on decisions that we need to make, and how to learn appropriately from customers, users, and others to make those decisions.</p>
<p>This may seem obvious and yet it often goes wrong in industry UX practice. For example, a classic usability test to find usability issues may go wrong if there is no process in place to act on and fix the issues that are found, or if management will simply dismiss them as “by design” or “won’t fix.” Likewise, a customer satisfaction (CSat) tracker is of no use unless there is a process in place to do something with the results.</p>
<p>To focus on decisions doesn’t mean to reduce everything to an A/B test or a specific product decision. A “decision” may mean a product action such as changing a feature but it may also mean to take some other action. For instance, if CSat drops on a tracking survey, an appropriate decision may be to launch a follow-up survey, to do usability testing, or to conduct depth interviews.</p>
<p>The important point is to have a decision process in place before research commences. That helps both to frame the research and to know what to do with it rather than simply hoping for “impact.”</p>
<h3 id="heading-rigor-2-quality-data-and-methods">Rigor 2. Quality Data and Methods</h3>
<p>Once we know what decision(s) to influence, we need high quality data and appropriate research methods. This area is most similar to traditional conceptions of “rigor” although I would emphasize a few differences. For one thing, as I noted above, it doesn’t imply rigid data collection or advanced methods.</p>
<p>Also, to address a decision point rigorously, we typically need multiple kinds of information. The proverbial “quant-qual sandwich" (alternating quantitative and qualitative projects) is one approach. Perhaps the most common question in this realm is “why?” Suppose customers prefer some potential feature on a survey. That answer is insufficient; we also need to know “why?” We may also wish to know, “what else?” and insight into “instead of what?” … for example, whether their preference comes at the expense of brand perception, another product of ours, an improved competitive position, and so forth.</p>
<p>From a methods point of view, this implies two things. First, the methods required may be straightforward. If multiple sets of data agree or disagree, there is no need for fancy analysis. Second, it implies that simply “getting an answer” is not enough. The point of having a specific decision frame (“Rigor 1” above") is not to exclude broader learning; rather, it is to set a target that we can hit directly and, ideally, surround with complementary insight.</p>
<h3 id="heading-rigor-3-effective-stakeholder-engagement">Rigor 3. Effective Stakeholder Engagement</h3>
<p>The third aspect of rigor is to ensure that you have both the relevant processes and skills to engage with stakeholders. Too often, I meet Quant UX researchers who hope that “the data speak for themselves.” But data sets don’t speak — that is our job.</p>
<p>In an ideal situation, a research team may have multiple members who divide the tasks of stakeholder engagement, research operations, analytics, and so forth. It is unreasonable to expect a single researcher to be highly competent in all of those areas.</p>
<p>Now, you might wonder, “Wait? Why is stakeholder engagement at level 3 instead of being a baseline level 1?” The reason is simple: <em>I’ve met too many UXRs, marketing researchers, and even academic researchers who are great at stakeholder engagement but who do not have the technical skills to deliver quality research</em>. Without quality research, great stakeholder engagement and storytelling too often end up as random walks or exercises in self-congratulation.</p>
<p>What is effective engagement? I would identify a few aspects. It is decision-oriented and the results are both clear and directly informative of the decisions to be made. Second, it pushes back on stakeholders to clarify the questions and say “no” when a proejct or question is infeasible. Third, it is unafraid to deliver negative results even when they are unpopular, such as, “No, users just don’t want that.” Fourth, it does this in a way that stakeholders will hear and value the results, and will want more in the future.</p>
<p>Is all of that a tall order and difficult to accomplish? Yes, and that’s why it is level 3 in my stack of “rigor”.</p>
<p><em>One other note</em>: <strong>too many organizations gaslight researchers into believing that it is a researcher’s fault for not having enough “impact”</strong> or poor stakeholder engagement. These relationships require two parties (at least) and failure, poor fit, or less-than-useful results do not arise because of researchers alone. Sometimes the best way to have better stakeholder relations is to change stakeholders (i.e., teams, organizations, jobs, or industries).</p>
<blockquote>
<p>As a side note, I’ll call out an implication of areas 1 + 2 + 3 when they are taken together: Rigor is not about the quantity of research, but about how well it addresses crucial problems.</p>
<p>It is better to determine the most important stakeholder questions and address them head-on than to produce an endless streaming of learning about users or answering small-stakes questions. And that leads us to the final area …</p>
</blockquote>
<h3 id="heading-rigor-4-attention-to-higher-order-strategic-decisions">Rigor 4. Attention to Higher-Order Strategic Decisions</h3>
<p>The final area of “rigor” builds on effective research and stakeholder engagement to deliver broader insight for a business. Unlike areas 1, 2, and 3 above, this is a relatively rare area but one that is rewarding to both organizations and researchers when and if (and only if) they are prepared for it.</p>
<p>This involves attention to issues including:</p>
<ul>
<li><p><strong>Opportunity costs of both product decisions and research engagement.</strong> If we do X, what does that mean for all of the things, T, U, V, and W that we did not do? If we spend time researching <em>hot question of the day</em>, what are we are not learning that could be more valuable?</p>
</li>
<li><p><strong>The asymmetric payoff matrix for all decisions and findings</strong>. For any given research result, there is some chance we will be wrong; and for any decision there is an expected benefit as well as potential cost. When we put those together, we can better inform product and business decisions. A full consideration of this area is far beyond the scope here, but I will note two extremely common failure points in this area: (a) a presumption that a product, feature, service, or business will happen, paired with a research demand to “validate” it, failing to consider the alternative that the business may be unneeded or impossible; and (b) a presumption that research should occur just because a question is “important.”</p>
</li>
<li><p><strong>Models for research allocation and staffing</strong>. With good stakeholder engagement, credibility, and skills in broad research for decision making, we can turn those skills to address questions such as, “What should we be doing with research?”, “What are the things we don’t know?”, and “What is the right mix of research staffing?” The skills needed to answer those questions are exactly the same as to answer any other research question. If you’re thinking, “wait! that would be a massive research project” then I’ll point out again that research does not require “statistical significance” or large data sets. Instead, it requires an important decision plus learning to answer it … and even when an answer is imperfect, the expected value of <em>not</em> answering it is much worse than getting an approximate and useful answer.</p>
</li>
</ul>
<p>There is <strong>one particular failure mode of “strategic UX research”</strong> that I would like to call out: research into product strategy that is untethered to any particular decision or decision process, and that instead is merely hoping to find influence by calling itself strategic. Too many projects in “<em>foundational research</em>” fall into this category. They learn a lot about customers but do not have the decision orientation, stakeholder buy-in, quality data, and attention to asymmetric results that are required actually to inform strategy. Sadly, such projects all-too-often end up with the UX researchers showing up like junior product managers advocating for some particular direction in hopes they will win a battle for influence.</p>
<p>A common question about Level 4 strategic research is, “how do I get there? how can I sell it?” My take is that it can’t and shouldn’t be pressed as a direction by researchers. Instead, it requires (a) building the skills needed to do it, such as depth in game theoretic thinking, (b) great stakeholder engagement based on prior, more focused success, and (c) waiting for the right time and set of questions.</p>
<hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>I’ll boil this post down to two sentences:</p>
<p><strong>UX Research rigor not a property of methods or data. Instead, it is about informing effective decisions, in conditions of uncertainty, through appropriate learning from our users and customers.</strong></p>
<p>If I had to emphasize one skill for effective and rigorous research — and really, it is a frame of thought more than a skill as such — it would be to ask “what if?” incessantly. What if … I used 4 small sample methods instead of 1 large project? What if … the answer from customers is “no”? What if … we didn’t do this project? What if ... we shipped this feature or this product and we are wrong?</p>
<p>When that orientation is paired with individual skill in data collection and analysis, you will be well on the way to having truly “rigorous” research!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770145888684/9b27a0ea-150c-4f47-94df-7bb1e5e1c012.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Qualitative Inquiry Can Make Quantitative Work More Meaningful: But Which Comes First?]]></title><description><![CDATA[Kelly Moran
[Note from Chris: This week I've invited a post from Kelly Moran, a longtime colleague and exceptionally thoughtful and experienced qualitative researcher and anthropologist. Many of us are familiar with the qual/quant "sandwich" that alt...]]></description><link>https://quantuxblog.com/qualitative-inquiry-can-make-quantitative-work-more-meaningful-but-which-comes-first</link><guid isPermaLink="true">https://quantuxblog.com/qualitative-inquiry-can-make-quantitative-work-more-meaningful-but-which-comes-first</guid><category><![CDATA[qualitative data and quantitative]]></category><category><![CDATA[#Ux research]]></category><category><![CDATA[UX]]></category><category><![CDATA[quantux]]></category><dc:creator><![CDATA[Kelly Moran]]></dc:creator><pubDate>Thu, 20 Nov 2025 19:11:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762792658523/b459dc13-78ce-4758-b5fc-b06a8c441e6b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Kelly Moran</p>
<p>[<em>Note from Chris: This week I've invited a post from Kelly Moran, a longtime colleague and exceptionally thoughtful and experienced qualitative researcher and anthropologist. Many of us are familiar with the qual/quant "sandwich" that alternates research approaches. Kelly shares deeper thoughts about how to get the sequence right, and I recommend these considerations to all UX researchers.</em>]</p>
<p>In the data-obsessed world we live in, companies are constantly reaching for numbers as the foundation for improving the performance of their products and services. A commitment to gathering and analyzing user data is a critical component for building an understanding of how, and whether, we’re meeting customer needs. Harnessing this information allows teams to move beyond assumptions, identify pain points, and strategically design solutions that enhance usability, satisfaction, and ultimately, business outcomes. We know in UX research that numbers are often only part of the story.</p>
<p>My name is Kelly Moran, and I’ve been working on applying research to business and consumer problems since the early 00’s. I have an MS degree in Applied Anthropology and I’ve worked in both consulting and in-house, most recently on Google Search. I’m currently leading Experience Research at the global experience consulting company, Geniant.</p>
<h3 id="heading-mixing-methods">Mixing Methods</h3>
<p>Combining quantitative (quant) and qualitative (qual) research methods is a hot topic right now, and for good reason. A common question is which one should come first. I often hear the perspective that starting with quant will bring up questions that the team can then dig into with qual research for a deeper understanding of the “why.” This is a great approach. By identifying <em>what</em> is happening through quantitative data (the numbers and counts), teams can then use qualitative methods (the rich, descriptive, word-based data) to explore the motivations, context, and underlying reasons behind those metrics. It allows for a targeted investigation into the phenomena the numbers highlight. But sometimes flipping that script can ensure you're measuring the right things in the first place, or perhaps more accurately, that the quantitative measurements you are taking truly reflect the real-world behaviors and experiences you intend to capture.</p>
<p>This alternative approach (starting with qualitative research) can be incredibly powerful for checking your underlying assumptions and refining your data collection instruments before a large-scale quantitative study is even launched. By first engaging with a smaller group through observation, interviews, or other qualitative methods, you gain a deep, nuanced understanding of the landscape. This initial insight can reveal blind spots in your current metrics or suggest entirely new categories of data that should be tracked. In essence, starting with qual lets you refine the <em>questions</em> you ask in your quant research, ensuring the resulting data is both accurate and meaningful.</p>
<p>I have an illustrative example from some consulting work that demonstrates the value of a qualitative-first approach. I start by laying out the problem as business saw it, the response research took, what we learned, and how we recommended a larger change to the way the business approached understanding and building for their own team.</p>
<h3 id="heading-responding-to-poor-metrics">Responding to Poor Metrics</h3>
<p>A company that services loans wanted to improve the metrics coming in from their call center. They had a team of agents for borrowers to reach out to by phone with questions or for general service on their loans, and at the end of each call, agents were required to enter a reason for the call into their call management system. They had a list of about half a dozen options to choose from. The reason “make a payment” was selected over 80% of the time. And this made sense. Paying a loan is important, and hopefully every borrower is making frequent and timely payments. But the company had put a lot of work into making it easy for people to pay their loans online. Loan payment should be a straightforward process, yet according to the metrics, it was taking a lot of time for call center agents to get through all these calls processing payments for customers over the phone. Relieving this load from the agents would decrease the wait time for borrowers calling in about other matters.</p>
<p>So they kept working on improving the online payment process.</p>
<p>And they kept seeing “make a payment” as the dominant reason borrowers called in.</p>
<p>They began to wonder if they needed to work this from another angle. Shifting your mindset is a great way to approach a sticky problem. They decided making the software easier on the <em>agent</em> side might at least help agents get through those payments faster.</p>
<p>They engaged with my team to observe agents in the call center so we could make context-informed design recommendations for the agent-facing software.</p>
<h3 id="heading-what-we-learned-was-surprising">What we learned was surprising</h3>
<p>We ended up listening in on 98 calls over a few days of observation with several agents. We did see opportunities to improve the design of the agent software, but we also learned something else.</p>
<p>In our observations, the frequency of calling in to make a payment was much lower than 80%. In fact it was under 50%. Instead, many borrowers were calling to <em>check</em> on a payment. These customers had used the website to make a payment but the system had a delay in showing funds applied (a banking issue we recommended they address), so borrowers were calling to be sure their payment had gone through or at least to ask that it not be counted as late, as the delay was out of their control.</p>
<p>Agents would assure the customer that all was well, and at the end of the call, they’d check the “Make a Payment” box because “Reassure the Borrower” was not an option. Nothing else on the reasons list was close enough.</p>
<p>The data was wrong because the selection options did not accurately reflect the real world.</p>
<p>The fix was multi-faceted. On the one hand, they needed to either address the delay in applying payments or at least provide clearer messaging that a payment was being applied and that no late fees would result from the processing time. A sophisticated solution would include a call routing system that identified a caller by phone number, registered that their account had recently had a payment submitted online, and provided a recorded message that the payment was in process. This alone could have brought their “make a payment” numbers down, as perhaps some callers would hang up before reaching an agent.</p>
<p>The agent-side fix would not only improve their “make a payment” metric but also provide a more accurate picture of why customers call in by improving the data being collected. They needed to reassess the list of reasons for calls, which our qualitative data provided a great starting point for. On the other hand if they had begun their original effort to collect data on "reasons for call” with a qualitative review of those calls they could have had a more accurate picture from day 1 of metrics collection.</p>
<h3 id="heading-there-is-real-power-in-rotating-between-qual-and-quant">There is real power in rotating between qual and quant</h3>
<p>The benefits of pairing quantitative and qualitative research methodologies is clear, but the sequencing can feel challenging. While the quant-first approach, using numbers to raise questions for qual to answer, is valuable for targeted follow-up, the example from the loan servicing company demonstrates a practicality in leading with qualitative inquiry. Had the company implemented a qualitative discovery project before they implemented their call categorization metrics they could have saved a lot of time trying to fix an online payment system that was already working (albeit missing critical follow-up messaging). And without my team happening to be in the right place observing for a different design purpose, the company would have continued to invest in the wrong solutions.</p>
<p>Qualitative research can serve as a vital diagnostic tool, ensuring your quantitative instruments are calibrated to the real world. By doing qual first, you don't just dig deeper into existing data; you ensure you are measuring the right things in the first place, leading to truly effective, context-informed solutions.</p>
<h3 id="heading-so-which-should-you-do-first">So which should you do first?</h3>
<p>As always with research, it depends. Is your team confident they have a clear enough understanding to create meaningful categories for your quantitative data? Is there any reason why they could not pause for a qualitative review of the landscape? Sometimes secondary datasets can provide useful context for this work. And criticality, <strong>what risks</strong> might there be in collecting inaccurate or incomplete data? Talk through these questions with your team and decide how to create the best combined quant/qual plan for your workstream. If confidence is high, timing is tight (and second guess that decision, always), and the risk is low, suggest circling back once metrics start rolling in to see how a qualitative project could add clarity. Otherwise, get out there and start defining your customers’ reality with descriptive data.</p>
<hr />
<p>The cover image is adapted from a photo by <a target="_blank" href="https://unsplash.com/@jemsahagun?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Jem Sahagun</a> found on <a target="_blank" href="https://unsplash.com/photos/danbo-standing-on-laptop--kqC3rZEMBI?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Things I'm Hearing about UX Research]]></title><description><![CDATA[In the past few months, I’ve had many conversations with senior folks across a variety of MAMANG and similar companies. These are all UXR managers or senior ICs (L5 and up, largely L6-L8 if you’re familiar with those levels). I expected to hear a var...]]></description><link>https://quantuxblog.com/things-im-hearing-about-ux-research</link><guid isPermaLink="true">https://quantuxblog.com/things-im-hearing-about-ux-research</guid><category><![CDATA[uxresearch]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Wed, 20 Aug 2025 17:25:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/zoCDWPuiRuA/upload/697289b7c3a47322e84e5f0f5468b1af.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the past few months, I’ve had <strong>many conversations with senior folks</strong> across a variety of MAMANG and similar companies. These are all UXR managers or senior ICs (L5 and up, largely L6-L8 if you’re familiar with those levels). I expected to hear a variety of one-off, situational discussions, but to my surprise, <strong>the themes have been surprisingly coherent</strong> across my conversations.</p>
<p>In this post, I want to share what I’m hearing. This is reporting and <strong>I’m not arguing for or against any particular claim.</strong> Each claim could be a contentious discussion, and I’m not inclined nor do I feel sufficiently broadly informed to engage in the arguments in detail.</p>
<p>Put differently, <strong>my goal is simply awareness</strong> — and closely related, I hope that UXRs who feel isolated, gaslit, anxious, or confused will understand that they are not alone.</p>
<p>I will preface this with a <strong>caveat</strong>: <strong>these themes are what I am hearing</strong>, but I am not a random or representative sample. My connections and I self-select to chat, exactly because we often agree. Nevertheless, the views I’m hearing from senior UXRs are important in the UX industry.</p>
<hr />
<h2 id="heading-1-the-job-market-is-chaotic-amp-unpredictable-for-uxrs">(1) The Job Market is Chaotic &amp; Unpredictable for UXRs</h2>
<p>I expect that no one will be surprised by this section.</p>
<p>For various reasons — ranging from layoffs to dissatisfaction — many of the UXRs I talk with are looking for new positions, and are quite frustrated by the experience. I’m hearing four primary trends:</p>
<ul>
<li><p>A feeling that “there are no jobs”</p>
</li>
<li><p>Over leveling, with everyone seeking higher positions</p>
</li>
<li><p>More emphasis on specific skills, especially “quant” and “mixed methods” (maybe without understanding them)</p>
</li>
<li><p>Extreme asymmetry in openings between tech hubs — especially Silicon Valley, and to some extent Seattle and New York — versus most other locations and remote</p>
</li>
</ul>
<p>As for <strong>over leveling</strong>, there is a trend towards acceleration of career levels, with folks expecting promotion every few years at least. This leads to two problems: (1) folks who are in over their heads when their breadth of experience (in methods, approaches, products, and politics) doesn’t match expectations of senior stakeholders, and (2) fewer job openings as they move up, paired with higher demands, leading to lower satisfaction. We say much more about that in Chapter 14 of the <a target="_blank" href="https://www.amazon.com/Quantitative-User-Experience-Research-Understanding/dp/1484292677">Quant UX book</a>.</p>
<p>I am also hearing a <strong>lot of demand for quant skills</strong>, with positions often listing something “quant” added onto general UX research skills. However, it’s difficult to sort out the extent to which hiring managers actually understand what they are seeking in these cases. Some managers may expect little more than general attention to UX metrics (see <a target="_blank" href="https://quantuxblog.com/how-to-make-heart-metrics-work-in-practice">Kerry Rodden’s excellent post</a>) while others may imagine — quite unrealistically — that they can find a UXR who spans everything from qualitative fieldwork to data science logs analysis. Mostly, my advice here is for hiring managers to be very specific (see the <a target="_blank" href="https://www.amazon.com/Quantitative-User-Experience-Research-Understanding/dp/1484292677">Quant UX book</a>) and for candidates to ask lots of questions and not oversell their quant skills. To be fair, what folks tell me about this may be biased due to my own experience.</p>
<p>There are some signs of recovery in the market but it is appearing tepid. My main recommendation for folks outside the Bay Area is to <strong>consider the idea of relocating</strong> there or at least to Seattle or another major tech hub. <em>Yes, I know relocation is painful, expensive, and perhaps even unfair</em>. I’m only saying that because it is the reality I am seeing.</p>
<blockquote>
<p><em>TBH, I took the relocation advice once myself. In 2011, I was looking for new positions and the market in Seattle was bad at that time. Google offered me positions in Mountain View and New York, and I ended up moving — for 3 years, until transferring back to Seattle. In retrospect, it was a great career choice.</em></p>
</blockquote>
<hr />
<h2 id="heading-2-yet-hiring-managers-dont-see-the-candidates-they-expect">(2) Yet Hiring Managers Don’t See the Candidates They Expect</h2>
<p>In social media (e.g., LinkedIn) there is a pervasive sense that UXR jobs are few, difficult to land, and swamped with applications. Given that, <strong>I was surprised by several hiring managers (HMs) who perceive a lack of UXR candidates.</strong> Not a single HM said they have <em>too many</em> candidates.</p>
<p>The three main things I heard about hiring are:</p>
<ol>
<li><p>There are some jobs although headcount is tight</p>
</li>
<li><p>HMs don’t see enough candidates</p>
</li>
<li><p>Candidates don’t have the right skills</p>
</li>
</ol>
<p>The first point is pretty obvious. Hiring managers often report that they are able to hire again, but they have <strong>substantially reduced headcount</strong>, compared to highpoint years such as 2022.</p>
<p>The second claim, from several folks, greatly <strong>surprised me: that they do not have enough candidates</strong>. For instance, I heard of one senior position at a MAMANG company that received fewer than 10 candidates, and among those applicants, the HM judged that none was qualified. To be clear, this is <em>not</em> due to an overall lack of applicants; I’ve heard this from managers at two of the top five largest tech companies, where the application pools are overflowing with candidates. And yet few filter through to HMs.</p>
<p>I don’t have a good explanation for this, although <strong>one important factor is how recruiting has changed</strong> since COVID and after the big rounds of layoffs in 2023 &amp; 2024. Many long-tenured and knowledgeable recruiters lost their jobs or changed companies.</p>
<p>By the time hiring started to resume, <strong>companies had lost the recruiting skills and knowledge needed to understand, screen, assess, and attract specialized candidates such as Quant UXRs</strong>. It is not a position that can be reduced to a list of qualifications such as knowledge of methods. Instead, it is crucial for recruiters to understand how quant skills relate to stakeholder needs. When they understand that, they can improve screening by lightly probing candidates in initial interviews to see whether they are able to adapt and explain concepts and impact clearly. It takes time, experience, and support for <em>recruiters</em> to develop those skills. Additionally, as recruiter and hiring managers turn over, they lose the connections and understanding that previously led to alignment on potential candidates.</p>
<p>Meanwhile, the deluge of job-seekers — multiplied by AI and resume-padding — has overwhelmed recruiters. The difficulty for recruiters is compounded by rapidly changing requirements from upper management, fluctuating numbers of openings, and consequent pressure from hiring managers to fill positions quickly. An open position one week may be repurposed the next week, abandoning candidates who are in process … and discouraging recruiters from investing time in the next round of candidates.</p>
<p>The net result: <strong>the candidates who make it to hiring managers are far more random</strong>, and less well-screened than they were in pre-COVID times.</p>
<blockquote>
<p><strong>Recommendation</strong> for current, senior Quant UXRs: have you done 10+ interviews with candidates? Reach out to your recruiter(s) and ask whether they would sync with you about the experience. Perhaps you could sit down for an hour to review candidates in their pipeline. You might discuss the things you would look for on CVs, or simple phone screen questions with good and not-so-good example answers. This may boost the quality of candidates, and make a recruiting friend!</p>
</blockquote>
<hr />
<h2 id="heading-3-many-senior-uxrs-are-disillusioned-by-colleagues">(3) Many Senior UXRs Are Disillusioned by Colleagues</h2>
<p><em>I love the UXR community — so this section is painful for me. But I am sharing it as I heard it.</em></p>
<p>This is a short section and makes me sad. I have increasingly heard from senior UXRs (L6 aka “Staff Researchers” and up) that they view colleagues and junior researchers as lacking skills. They claim things like:</p>
<ul>
<li><p>Research is rudimentary and responds only to direct stakeholder questions, missing the larger picture</p>
</li>
<li><p>Research obtains obvious results and then is oversold as being very important</p>
</li>
<li><p>Research design has significant flaws such as skewed samples, leading questions, bad survey panel data, dubious methods, and the like</p>
</li>
<li><p>Specifically on the quant side, research may be primarily “procedural” in applying advanced methods or a great deal of code and analysis, but is applied to low quality or largely irrelevant data</p>
</li>
<li><p>Colleagues use questionable tools — LLMs are often called out — to write reports, deliver unreliable findings, and the like</p>
</li>
</ul>
<p>This is often summed up as, “they have no idea what they’re doing” or “they just crank out meaningless reports.”</p>
<p>Now, <strong>I’m not making those claims, just reporting them</strong>. My personal impression is that we’re seeing a combination of several things:</p>
<ul>
<li><p><strong>Everyone is stressed and stretched</strong>. Junior UXRs do not have the mentoring or time to develop skills on the long time horizons that used to be common. Senior UXRs take out their frustrations by complaining about research quality.</p>
</li>
<li><p><strong>Demands are increasing</strong>, as described above about management expectations</p>
</li>
<li><p><strong>Research is becoming more difficult to do, with less support</strong>. This comes from a combination of difficulty recruiting participants, low quality survey panels, and shrinking budgets for research.</p>
</li>
<li><p>The <strong>breadth of UX methods has grown</strong> such that no one can be expert in everything; and yet stakeholders may well demand that any UXR tackle any kind of research.</p>
</li>
<li><p>As mentioned above, <strong>over-leveling</strong> causes both real problems and also interpersonal resentment.</p>
</li>
</ul>
<p>My main recommendation here is for everyone to try to look for the best in others.</p>
<blockquote>
<p><strong>Managers</strong>: help your team do less in terms of breadth, and instead with more partnership in depth. Instead of having 4 UXRs run 8 different projects … try having 2 teams of 2 UXRs focus on 4 more important projects. You will have less “coverage” but that will be strongly offset by having much greater depth on the most important projects. 2 UXRs working together have a multiplicative effect. And it will build cohesion, develop skills, and insulate your team from shocks of turnover and the like.</p>
</blockquote>
<hr />
<h2 id="heading-4-and-uxrs-are-too-often-disillusioned-especially-llm-hype-from-upper-management">(4) ... and UXRs are too often disillusioned, especially LLM hype from upper management</h2>
<p>LLMs were an important topic in <strong>every single conversation</strong> with senior UXRs. Typically AI / LLMs emerged as the #1 issue of discussion as a conversation progressed.</p>
<p>The most common concern is this: <strong>UXRs feel that LLMs are being pushed into products without any clear understanding of user needs</strong> for them.</p>
<p>Specifically, UXRs often see, feel, or believe one or more of the following:</p>
<ul>
<li><p>Teams are told to “find a need” under a predetermined assumption that LLMs must be valuable</p>
</li>
<li><p>If UXRs find that customers do <em>not</em> want an LLM feature, management won’t accept the finding</p>
</li>
<li><p>If UXRs push back, they believe their own jobs are at risk in an awaited next round of layoffs</p>
</li>
<li><p>They worry that management doesn’t understand LLMs to begin with, so it is all an exercise in futility</p>
</li>
<li><p>They fear (or observe) this LLM push will erode product value, user satisfaction, and brand trust</p>
</li>
</ul>
<p>These UXRs view the continual search for “ways to use LLMs” as a fool’s errand when there are urgent and obvious unmet needs and pain points in products. They may be seeing decreasing user satisfaction, pushback from users about AI, security threats, worsening UI experience, declining overall code quality, and immediately addressable needs that users are requesting — all of which are ignored in the promise of some AI utopia to come. <strong>Doing research on <em>yet-another-AI-concept</em> can be demoralizing in the face of actual user needs</strong>.</p>
<p>To be clear, <em>I’m not saying that there are no uses for LLMs</em>. I’m saying that AI does not change everything overnight, and that management expectations for AI are often out of alignment with users and users’ needs.</p>
<p><strong>This mismatch between management expectation and the reality of interacting with users can lead to UXRs responding with helplessness, apathy, cynicism, dishonesty</strong> such as telling management what it wants to hear, and similar and other ultimately self-destructive responses.</p>
<blockquote>
<p><strong>Recommendation</strong>: if your research projects appear unrealistic, short-sighted, unlikely to ship or have impact, or even border on being delusional, find a way to add something that delivers value in another way. That might be foundational research you can do “for free” (and perhaps not even report), it might be learning about method to build your skills, or it might be following up on prior research. Research plans almost always offer the flexibility to learn more than simply meeting immediate goals. Best case, the team learns something. But the worst case is also good: you learn something!</p>
</blockquote>
<hr />
<h2 id="heading-but-uxrs-love-being-uxrs-so-cautious-optimism">But UXRs Love Being UXRs: So, Cautious Optimism</h2>
<p>The final thing I heard across my conversations was this: <strong>UXRs love being UXRs</strong>. Much of the anxiety concerned whether they would be able to continue as UXRs (or perhaps have to look for jobs in data science or PM); and whether the role of UXR could continue to look the way it has in the past, with the excitement and enjoyment of learning about users from direct interaction as well as data.</p>
<p>I’ve often said that UX Researcher (and its variants such as Quant UXR) are the best job in the world … for the right person. For people who love learning about many different products, tacking ambiguous research goals, doing research well, meeting customers and helping to solve their needs, and working in generally enjoyable and good positions, it is difficult to imagine a better career. <strong>Ultimately, learning from users and delivering better products must deliver high value on average</strong>.</p>
<p>So I expect the UXR situation to improve both in terms of jobs and research demands. <em>When</em>? That’s harder to say … but each of us can help that day come sooner. I hope these reflections and recommendations might help.</p>
<blockquote>
<p><strong>Final personal note</strong>: to increase well being and generally build community, I’ve organized a Zen meditation group for the Tech — especially UXR — community. This is not a sales pitch (and we don’t sell anything!) but just my personal note. There are many options to well being apart from Zen, but if you’re interested, details are at <a target="_blank" href="https://tczen.org"><strong>https://tczen.org</strong></a> Join a session or sign up for the newsletter.</p>
<p>More generally, treat your colleagues, the world, and yourself kindly. We’re all in this together!</p>
</blockquote>
<p>For much more discussion, to meet other UXRs, and to hear different perspectives, I also encourage you to join us for Quant UX Con 2025 in November. <a target="_blank" href="https://quantuxcon.org">Find details here</a>!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748018267452/49cc88ed-1e5b-4359-8ac5-a38db6c5bd11.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Two Year Highlights of the Blog]]></title><description><![CDATA[I started this blog in August 2023. After 2 years I’d like to highlight a few posts. I hope these will have continuing interest for anyone who missed them; and also may be good introductions for new subscribers (“subscribe” is always free here; click...]]></description><link>https://quantuxblog.com/two-year-highlights-of-the-blog</link><guid isPermaLink="true">https://quantuxblog.com/two-year-highlights-of-the-blog</guid><category><![CDATA[quantux]]></category><category><![CDATA[#Ux research]]></category><category><![CDATA[survey research]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Thu, 14 Aug 2025 15:15:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754247857655/9d8a60ed-5e97-4bbe-a0b7-7e73dd3da49e.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I started this blog in August 2023. After 2 years I’d like to <strong>highlight a few posts</strong>. I hope these will have continuing interest for anyone who missed them; and also may be good introductions for new subscribers (“subscribe” is always free here; click the envelope ✉️+ icon in the upper right!)</p>
<p>I sort these articles into categories of:</p>
<ul>
<li><p><em>Tech Community Reflections</em></p>
</li>
<li><p><em>Quant Methods</em></p>
</li>
<li><p><em>Method Critiques</em></p>
</li>
<li><p><em>Career Advice</em></p>
</li>
<li><p><em>Distinguished Guests</em></p>
</li>
</ul>
<p>I’ve added a brief note about each topic as a preview or comment. I hope you find something new and provocative in these articles!</p>
<hr />
<h2 id="heading-tech-community-reflections">Tech Community Reflections</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754855222853/0f14b40b-fb8d-4666-b993-d6e527181333.png" alt class="image--center mx-auto" /></p>
<p><strong>The End of Tech as a Big Family</strong>: <a target="_blank" href="https://quantuxblog.com/the-end-of-tech-as-a-big-family">https://quantuxblog.com/the-end-of-tech-as-a-big-family</a>. This post reflects on the personal anxieties and changes in Big Tech that were exposed during recent layoffs.</p>
<p><strong>We’re Far from AGI:</strong> <a target="_blank" href="https://quantuxblog.com/were-far-from-agi">https://quantuxblog.com/were-far-from-agi</a>. As a psychologist and sometimes philosopher, I explain why proponents of AGI and “superintelligence” don’t understand what they’re claiming (or more precisely, they don’t understand what they are <strong>not</strong> discussing).</p>
<hr />
<h2 id="heading-quant-methods">Quant Methods</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754669737180/01dfd98c-d94b-40e9-b22a-f8ca15d4e769.jpeg" alt class="image--center mx-auto" /></p>
<p><strong>Multidimensional Sentiment Analysis</strong>: <a target="_blank" href="https://quantuxblog.com/multidimensional-sentiment-analysis-part-1">Part 1</a>, <a target="_blank" href="https://quantuxblog.com/multidimensional-sentiment-analysis-part-2">Part 2</a>. If I were to sort methods into quadrants on a 2×2 grid, arranging them by powerfulness vs. frequency of usage, then multidimensional sentiment analysis would score as a “high opportunity” — very powerful but surprisingly rarely used. I’d like to change that!</p>
<p><strong>Easy MaxDiff in R</strong>: <a target="_blank" href="https://quantuxblog.com/easy-maxdiff-in-r">https://quantuxblog.com/easy-maxdiff-in-r</a> . If you’d like to understand MaxDiff from the ground up, this short code explainer may help (note: the code is for <em>educational</em> purposes, not production). It looks at the observations that MaxDiff collects, and basic statistics for that data.</p>
<p><strong>Individual Scores in Choice Models, Part 3: Respondent Segments</strong>: <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-3-respondent-segments">https://quantuxblog.com/individual-scores-in-choice-models-part-3-respondent-segments</a>. I examine one way to segment MaxDiff data. But even more, this article is about how <em>segmentation is usually misunderstood</em> and what it actually does.</p>
<p><strong>Individual Scores in Choice Models, Part 4: Inspecting Model Fit with RLH</strong>: <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-4-inspecting-model-fit-with-rlh">https://quantuxblog.com/individual-scores-in-choice-models-part-4-inspecting-model-fit-with-rlh</a>. I look at the RLH fit statistic in conjoint analysis and MaxDiff. Using basic math, theory, and code, I show why <em>common heuristics to interpret RLH are misleading</em>. RLH is diagnostic but cannot “filter” respondents.</p>
<hr />
<h2 id="heading-method-critiques">Method Critiques</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754669797978/785aa112-c9fe-4909-8c4b-ab9aa1d1a552.jpeg" alt class="image--center mx-auto" /></p>
<p><strong>Surveys and the “True Score” Mistake</strong>: <a target="_blank" href="https://quantuxblog.com/surveys-and-the-true-score-mistake">https://quantuxblog.com/surveys-and-the-true-score-mistake</a>. This article explains why <em>surveys are not about finding “the truth”, aka a latent score, in people's heads</em>. Instead surveys are about listening to people. (This article is also one of the ground for my criticism of synthetic data in the next article.)</p>
<p><strong>Synthetic Survey Data? It’s Not Data</strong>: <a target="_blank" href="https://quantuxblog.com/synthetic-survey-data-its-not-data">https://quantuxblog.com/synthetic-survey-data-its-not-data</a> This article explains why I believe <em>the concept of “synthetic data” is impossibly flawed</em> in multiple ways, including basic logic, statistics, and in regards to the scientific method.</p>
<p><strong>Critical Assessment of the Kano Model</strong>: <a target="_blank" href="https://quantuxblog.com/critical-assessment-of-the-kano-model-part-1">Part 1</a>, <a target="_blank" href="https://quantuxblog.com/critical-assessment-of-the-kano-model-part-2">Part 2</a>. If you are tempted to use the Kano Model to identify “delighter” products … I don’t recommend it. This pair of articles explains why not, and gives alternatives.</p>
<hr />
<h2 id="heading-career-advice">Career Advice</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754669833462/d33a541e-acd2-410b-931e-aa6a02de7c7d.jpeg" alt class="image--center mx-auto" /></p>
<p><strong>Skills Combination for Quant UX Applications:</strong> <a target="_blank" href="https://quantuxblog.com/skills-combination-for-quant-ux-applications">https://quantuxblog.com/skills-combination-for-quant-ux-applications</a>. I’ve interviewed hundreds of candidates and, for several years, coordinated the standard Quant UX hiring criteria at Google. This article <em>summarizes the skills needed to be a Quant UXR</em>, and is also a good preview of the <a target="_blank" href="https://quantuxbook.com">Quant UX book</a>!</p>
<p><strong>Recommendations for Quant UX Interview "Portfolio" Presentations</strong>: <a target="_blank" href="https://quantuxblog.com/quant-ux-interview-portfolio-presentations-recommendation">https://quantuxblog.com/quant-ux-interview-portfolio-presentations-recommendation</a>. If you are asked to give a Quant UX research presentation — sometimes confusingly called a “portfolio” presentation — this is how to approach it. The short version: <em>give a research presentation that interests you</em>; don’t try to guess some “right” content.</p>
<hr />
<h2 id="heading-distinguished-guests">Distinguished Guests!</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754669945252/01b1c51e-9ff6-418f-9040-3f5ec6a588b7.jpeg" alt class="image--center mx-auto" /></p>
<p><strong>How to make HEART metrics work in practice</strong>: <a target="_blank" href="https://quantuxblog.com/how-to-make-heart-metrics-work-in-practice">https://quantuxblog.com/how-to-make-heart-metrics-work-in-practice</a> One of the founders of the Quant UX discipline, Kerry Rodden examines the evergreen and useful HEART framework, detailing practical advice to apply it. <em>HEART is my all-time single favorite Quant UX framework</em>, and a memorable heuristic to draw on in many, many research contexts.</p>
<p><strong>Be a T-shaped Quant UXR: How Doing Qualitative Research Made Me a Better Quantitative UX Researcher</strong>: <a target="_blank" href="https://quantuxblog.com/be-a-t-shaped-quant-uxr-how-doing-qualitative-research-made-me-a-better-quantitative-ux-researcher">https://quantuxblog.com/be-a-t-shaped-quant-uxr-how-doing-qualitative-research-made-me-a-better-quantitative-ux-researcher</a> Kitty Xu, cofounder of the Quant UX Conference, explains why Quant UXRs should also engage personally with qualitative research.</p>
<hr />
<h2 id="heading-more-reading">More Reading?</h2>
<p>Each article above has pointers to additional articles or references. Or check out:</p>
<ul>
<li><p>Hundreds of talks in archives of the <a target="_blank" href="https://quantuxcon.org">Quant UX Conference</a> … or <a target="_blank" href="https://events.ringcentral.com/events/quant-ux-con-2025-worldwide"><strong>join us in November</strong></a> 2025!</p>
</li>
<li><p>With my coauthors: the <a target="_blank" href="https://www.amazon.com/Quantitative-User-Experience-Research-Understanding/dp/1484292677">Quant UX book</a>, the <a target="_blank" href="https://www.amazon.com/Marketing-Research-Analytics-Use/dp/3030143155">R book</a>, and/or the <a target="_blank" href="https://www.amazon.com/Python-Marketing-Research-Analytics-Schwarz/dp/3030497194">Python book</a></p>
</li>
<li><p>And I hope you’ll subscribe to this blog, if you haven’t already</p>
</li>
</ul>
<p>Happy reading!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755288272665/03aa908f-dd87-4d51-a2c1-0a74113b6bad.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Friday Break: Book Recs, Science Fiction]]></title><description><![CDATA[This post is for newsletter subscribers, to recommend a few science fiction books. These are all entertaining and thought provoking. Also, each of them engages with themes are directly relevant today with regards to social structures such as politics...]]></description><link>https://quantuxblog.com/friday-break-book-recs-science-fiction</link><guid isPermaLink="true">https://quantuxblog.com/friday-break-book-recs-science-fiction</guid><category><![CDATA[books]]></category><category><![CDATA[scifi]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Fri, 01 Aug 2025 17:32:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/oMpAz-DN-9I/upload/e0a29ba63fdca4bf4500cef4b008c8fd.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This post is for newsletter subscribers, to recommend a few science fiction books. These are all entertaining and thought provoking. Also, each of them engages with <strong>themes are directly relevant today</strong> with regards to social structures such as politics and economics, and/or AI technologies.</p>
<p>I was fortunate to study writing with the <a target="_blank" href="http://blogs.scientificamerican.com/observations/can-science-fiction-save-the-world">late SF Grand Master, James Gunn</a>. He often made two points: that <strong>science fiction might save the world</strong> (by letting us explore hypotheticals to avoid and plan potential responses), and that <strong>all fiction should be entertaining</strong> (otherwise what is the point?)</p>
<p>To Gunn's points, each of these books is entertaining — and maybe each is helpful to save the world. All of these authors offer more to read if you like them. Several of the titles anchor series with 3+ books.</p>
<p>I’ll also acknowledge the counter-narrative to Gunn’s positive claims for SF: it also contributes to expectations and fantasies — such as those about AI — that are destructive when we absorb them uncritically. (IMO this is usually due to lack of understanding, when readers don’t understand that SF is not about predicting the future; it’s about exploring themes that are happening <em>now</em>.)</p>
<p>Jemisin's and Scalzi's books are reportedly filming as TV series (Corey's and Chiang's already have been filmed). Read the books first!</p>
<hr />
<p><strong>Ted Chiang</strong>. <em>Stories of Your Life and Others</em>. One of the best short story collections I've read. The title story is an astounding look at the limits of human experience. Read it first and then watch the movie adaptation, <em>Arrival</em>. <a target="_blank" href="https://www.amazon.com/Stories-Your-Life-Others-Chiang/dp/1101972122">Amazon</a>, <a target="_blank" href="https://www.powells.com/book/stories-of-your-life-and-others-9781101972120">Powells</a>.</p>
<p><strong>James S.A. Corey</strong>. <em>Leviathan Wakes</em>. First in a series filmed as <em>The Expanse</em>. A great look at a future that feels much more probable than than those often portrayed much of SF, with battling factions, politics, economies, billionaires, and solar system conflict. Read and then watch <em>The Expanse</em>. <a target="_blank" href="https://www.amazon.com/Leviathan-Wakes-James-S-Corey/dp/0316129089">Amazon</a>, <a target="_blank" href="https://www.powells.com/book/leviathan-wakes-9780316129084">Powells</a>.</p>
<p><strong>N.K. Jemisin</strong>. <em>The Fifth Season</em> (The Broken Earth Trilogy). It takes a while — perhaps 100 pages — to "learn how to read" this book. Stick with it! Is it fantasy or science fiction? Either way the entire series deserves its 3-in-a-row Hugo awards. <a target="_blank" href="https://www.amazon.com/Fifth-Season-Broken-Earth/dp/0316229296">Amazon</a>, <a target="_blank" href="https://www.powells.com/book/the-fifth-season-broken-earth-1-9780316229296">Powells</a>.</p>
<p><strong>Stephen Markley</strong>. <em>The Deluge</em>. This title is <em>not</em> genre SciFi; it is literary fiction set in the near future, but it is close enough for me to include it. A sprawling book about climate change, societal breakdown, power, and political action in the near term US, with multiple story lines that converge dramatically. <a target="_blank" href="https://www.amazon.com/Deluge-Stephen-Markley/dp/1982123109">Amazon</a>, <a target="_blank" href="https://www.powells.com/book/deluge-9781982123109">Powells</a>.</p>
<p><strong>John Scalzi</strong>. <em>Old Man's War</em>. This has one of the best and most memorable opening pages of any book. It is military style SF, but even if that's not your thing, you'll likely enjoy it. It is a fun read that maxes out on the factors of fast plot and entertainment in SF. <a target="_blank" href="https://www.amazon.com/Old-Mans-War-John-Scalzi/dp/0765348276">Amazon</a>, <a target="_blank" href="https://www.powells.com/book/old-mans-war-9780765348272/1-19">Powells</a>.</p>
<p><strong>Adrian Tchaikovsky</strong>. <em>Children of Time</em>. Besides being great space opera, this series has outstanding portrayals of non-human cognition. For those interested in AI and AGI, I recommend reading the series and considering what those themes tell us about AI. <a target="_blank" href="https://www.amazon.com/Children-Time-Adrian-Tchaikovsky/dp/0316452505">Amazon</a>, <a target="_blank" href="https://www.powells.com/book/children-of-time-book-1-9780316452502">Powells</a>.</p>
<p><strong>Robert Charles Wilson</strong>. <em>Spin.</em> I love Wilson's books for their portrayal of near-future society. Even more, his writing is especially humane in its focus on people. <em>Spin</em> is a great tale that starts one night when the stars all go dark. <a target="_blank" href="https://www.amazon.com/Spin-1-Robert-Charles-Wilson/dp/1250237513">Amazon</a>, <a target="_blank" href="https://www.powells.com/book/spin-9781250237514">Powells</a>.</p>
<hr />
<h3 id="heading-read-them-already-how-about">Read them already? How about ... ?</h3>
<p>If you like the books above, try some of these authors, according to similarities I see:</p>
<p><strong>Octavia Butler</strong> ... may appeal if you like Wilson or Jemisin. At least read <em>Parable of the Sower</em>.<br /><strong>David Louis Edelman</strong> ...may appeal if you like Tchaikovsky<br /><strong>Greg Egan</strong>... may appeal if you like Chiang (nb, Egan is sometimes very technical)<br /><strong>Ann Leckie</strong> ...may appeal if you like Corey<br /><strong>Alaistair Reynolds</strong>... may appeal if you like Corey or Tchaikovsky<br /><strong>Kim Stanley Robinson</strong>... may appeal if you like Wilson. Great mix of hard science and human themes.<br /><strong>Neal Stephenson</strong> ... no near neighbor. Uneven IMO, suggest starting with <em>Snow Crash</em> or <em>Cryptonomicon</em><br /><strong>Peter Watts</strong> ... may appeal if you like Tchaikovsky (although Watts’s worlds are substantially darker)<br /><strong>Martha Wells</strong> ... may appeal if you like Scalzi. The first <em>Murderbot</em> book has filmed &amp; aired on Apple TV.<br /><strong>Connie Willis</strong> ... may appeal if you like Wilson</p>
<hr />
<h3 id="heading-my-science-fiction-short-list-to-read-soon">My Science Fiction Short List, to Read Soon</h3>
<p><strong>R.F. Kuang</strong>: <em>Babel.</em> Have heard superlatives; saving it for the right time to read.<br /><strong>Arkady Martine</strong>: <em>A Memory Called Empire.</em> Said to be a space opera plus and diplomacy SciFi.</p>
<p>Happy reading!</p>
<p>P.S. I've also recommended <a target="_blank" href="https://quantuxblog.com/some-book-recs-literary-fiction">literary fiction</a>. Sometime I'll recommend mysteries, too!</p>
<p><em>Finally</em>, as always, this post was …</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750445067989/393fc6d6-35bd-4a6a-a06e-b22714ae4f30.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Present at Quant UX Con 2025!]]></title><description><![CDATA[Today I’m encouraging all readers of this newsletter / blog: send in an idea for Quant UX Con. I’m the chair of Quant UX Con and I’d like to demystify the Call for Presentations!
First, if you’re wondering whether you can do it … you can! Quant UX Co...]]></description><link>https://quantuxblog.com/present-at-quant-ux-con-2025</link><guid isPermaLink="true">https://quantuxblog.com/present-at-quant-ux-con-2025</guid><category><![CDATA[quantux]]></category><category><![CDATA[conference]]></category><category><![CDATA[uxresearch]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Fri, 27 Jun 2025 15:48:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1751037631761/f51e2298-a8f2-4492-b535-ee614c39ae8f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Today I’m encouraging all readers of this newsletter / blog: <strong>send in an idea for Quant UX Con</strong>. I’m the chair of <a target="_blank" href="http://quantuxcon.org">Quant UX Con</a> and I’d like to demystify the Call for Presentations!</p>
<p>First, <strong>if you’re wondering whether you can do it … you can!</strong> Quant UX Con is intended to be <em>useful</em>, and does not require that presentations are academic “contributions”, or theoretical, or advanced methods, or anything like that. As long as a presentation is something of interest to UX Researchers who use quant methods, it fits.</p>
<p>Second, <strong>the Quant UX Con audience is friendly</strong> and welcoming, and new presenters are especially welcomed! And uou can present from anywhere worldwide, around the clock in any timezone. Or join live at a watch party location.</p>
<p>Third, if you’re thinking, “my company doesn’t care about publishing, it won’t help me” … I can say this: <strong>presenting will help you</strong>! Maybe it won’t help in your current job, but it <em>will</em> help your confidence, learning, meeting people, and will quite possibly your next job. (My own publishing was quite important when I made two job changes in my career, to Google and to Amazon!)</p>
<hr />
<h2 id="heading-i-dont-have-anything-to-present">“I don’t have anything to present!”</h2>
<p>Ask yourself this question: <strong>what is something I learned in the past two years, that I wish I had known before?</strong> If you can answer that, then you probably have a presentation that will interest others!</p>
<p>Your talk might walk through a method, or give a literature review, or a case study, or a career reflection, or put together a panel discussion, or a code walkthrough … all of those are welcomed! You can find past talks here: <a target="_blank" href="https://www.quantuxcon.org/quant-ux-con/past-conferences">https://www.quantuxcon.org/quant-ux-con/past-conferences</a></p>
<p><em>Want more specific ideas?</em> Here are some ideas I would personally like to see, off the top of my head:</p>
<ul>
<li><p>A walkthrough of a favorite but little-known R (or Python) package</p>
</li>
<li><p>How to set up a local SQL server and IDE, so you can learn SQL on your own</p>
</li>
<li><p>Scanning old PDFs to get data from them</p>
</li>
<li><p>How we tackled [difficult problem X] and the lessons we learned</p>
</li>
<li><p>Getting started with GitHub: how and why to set up your own code repo</p>
</li>
<li><p>Integrated reporting with Quarto: creating reports with reproducible code</p>
</li>
<li><p>Why Bayes?</p>
</li>
<li><p>Connecting Excel to R — using the power of R directly from Excel</p>
</li>
<li><p>Kill the chart clutter — how to make cleaner, better, more useful charts</p>
</li>
<li><p>Round table on hiring: 4 hiring managers [or 4 candidates] discuss the UXR job market today</p>
</li>
<li><p>Keeping up with the bots, addressing survey panel issues today</p>
</li>
<li><p>An alternative to Powerpoint: making slides in Beamer / R markdown / Quarto …</p>
</li>
<li><p>Put it in a notebook! Boost your collaboration with Jupyter (or Colab, or Quarto, etc.)</p>
</li>
</ul>
<p>... and of course, traditional research methods discussions, innovations, think pieces, or specific research findings.</p>
<hr />
<h2 id="heading-do-it-now-but-wait-i-dont-have-time">Do It Now … “But wait, I don’t have time!”</h2>
<p>Yes, the CFP is due on Tuesday July 1 … but that is doable! Proposals require only an abstract with these pieces:</p>
<ul>
<li><p><strong>Summary</strong>: 2-5 paragraph abstract that describes the presentation</p>
</li>
<li><p><strong>Audience</strong>: who will be interested?</p>
</li>
<li><p><strong>Goal / takeaways</strong>: what will the audience get from the talk?</p>
</li>
</ul>
<p>Once you have a concept, I’d estimate the CFP itself might take 30-45 minutes. Perhaps 60 or 120 if you really want to polish it. (FWIW, I timed myself and wrote up my own very complete proposal, directly in the CFP form, in 20 minutes.)</p>
<p>And <em>you’ll hear it here first</em>: <strong>we might extend the CFP by a few days</strong>. That has <em>not</em> been decided, so don’t count on it — and go ahead and start. But if it takes you slightly longer, you may be OK! Or go ahead and submit now, and then update it after you polish it.</p>
<p>Or worst case, write it up now and save it for next year :) But better, send it in!</p>
<hr />
<h2 id="heading-how">How?</h2>
<p>Find the CFP form at: <a target="_blank" href="https://app.oxfordabstracts.com/stages/78930/submitter">https://app.oxfordabstracts.com/stages/78930/submitter</a></p>
<p>Thanks for considering it — we really do hope you’ll share ideas, and of course, please <a target="_blank" href="http://quantuxcon.org">join us in November</a>!</p>
]]></content:encoded></item><item><title><![CDATA[Synthetic Survey Data? It's Not Data]]></title><description><![CDATA[[I write this reluctantly, because (1) AI is split into factions and this post may not change any minds; (2) it may upset some well-intentioned researchers. On the other hand, I’ve been asked and hope it is helpful.]
There is widespread discussion of...]]></description><link>https://quantuxblog.com/synthetic-survey-data-its-not-data</link><guid isPermaLink="true">https://quantuxblog.com/synthetic-survey-data-its-not-data</guid><category><![CDATA[survey]]></category><category><![CDATA[quantux]]></category><category><![CDATA[uxresearch]]></category><category><![CDATA[synthetic data]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Wed, 18 Jun 2025 15:47:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/IEiAmhXehwE/upload/c94d1fad8398dba59aa3fe191c6e1bc2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>[I write this reluctantly, because (1) AI is split into factions and this post may not change any minds; (2) it may upset some well-intentioned researchers.</em> <em>On the other hand, I’ve been asked and hope it is helpful.]</em></p>
<p>There is widespread discussion of LLM-generated synthetic survey data and its utility. Some survey panel providers promote synthetic data as being fast and cheap and, they claim, as a way to “boost the prevalence” of infrequent respondent groups.</p>
<p>In this post I share my point of view on synthetic survey data — not because I want to wade into any AI wars, but because the topic arises repeatedly and colleagues have <em>asked</em> me to write a post.</p>
<p>Among other things, I argue in this article that:</p>
<ul>
<li><p>the concept of synthetic data is <strong>logically flawed</strong></p>
</li>
<li><p>synthetic data <strong>fails empirically</strong> to emulate human data</p>
</li>
<li><p>synthetic data <strong>cannot overcome sampling limitations</strong></p>
</li>
</ul>
<p>I include some references but this is not a comprehensive review. It only compiles my thoughts, in varying degrees of completeness, plus a few pointers to other folks’ work. You may agree with some arguments more that others — and that’s fine, I only hope you agree with <em>something</em> :)</p>
<blockquote>
<p><strong>Side note</strong>: this post discusses the utility of synthetic data, but that is only one consideration. I’ve described elsewhere why it is important to <a target="_blank" href="https://quantuxblog.com/four-areas-of-uxr-thinking-about-ai-llms">consider ethics, externalities, aesthetics, and social structures</a> alongside the utility of AI. However, proponents of synthetic data discuss it in terms of utility, so that’s where I focus discussion in this article.</p>
</blockquote>
<hr />
<h2 id="heading-the-concept-of-synthetic-data-is-logically-flawed">The Concept of Synthetic Data is Logically Flawed</h2>
<p>I see three fundamental <em>logical</em> problems with LLM synthetic survey data:</p>
<ul>
<li><strong>It rests on the common, yet I believe incorrect, assumption that surveys are about sampling a “true state of affairs”</strong> in the world. That view is imported from classical psychometrics where it is a simplifying assumption. But it is not what surveys do in the real world. As <a target="_blank" href="https://quantuxblog.com/surveys-and-the-true-score-mistake">I have explained elsewhere</a>, <em>surveys are a form of motivated communication</em> and they must be interpreted as such. They cannot be viewed as measurements of any particular “reality” that is accessible or meaningful apart from considering motivation.</li>
</ul>
<p>Thus, unless we are interested in the motivations of LLM systems, their data logically cannot replace human data. It doesn’t even matter whether their answers might in some way be “the same” … because <strong>the point of surveys is not to measure “the answer” in the first place</strong>. The point is to listen to people, and that requires … well, listening to people.</p>
<ul>
<li>The second logical problem is temporal: <strong>LLMs are trained on past data, whereas the goal of a survey should be to listen to people now</strong>. Even if an LLM has historical data aligning with our question, it is outdated as soon as it has been trained. Thus, synthetic data has no determinable relationship to what we want to know <em>now</em>. It might or might not be relevant; we don’t know.</li>
</ul>
<p>You might object, “what if I want data about something that other researchers have asked in the past?” That’s fine … but <strong>if those data already exist, you don’t need to go through the convoluted process of writing a survey that you hope will align with those data, and then subject that survey to an LLM</strong> that you hope will use the data to give responses to your survey, which you then hope will recreate the data. Instead, simply Google your question and access existing data directly.</p>
<ul>
<li>The third problem concerns the domain of inquiry: <strong>the space of potential business questions is infinite, but existing data is finite</strong>. Thus we may expect that most questions (the infinite space) have not been answered … especially when we are working in a new product area. Although LLMs create novel data on demand, there is no logical reason to expect that their statistical models will infer any particular novel truth — and, indeed, LLMs are not even designed to represent truth <em>within</em> their training sets. [<em>Mathematical note</em>: there are <a target="_blank" href="https://www.scientificamerican.com/article/strange-but-true-infinity-comes-in-different-sizes/">infinities of differing sizes</a>. Even if an LLM can infer within one infinity of data, it doesn’t necessarily infer within all infinities of data. I’ll set that aside.]</li>
</ul>
<p>To summarize: there is no need to do research when the answer to your question already exists. But <strong>you can’t find out whether an answer to your question exists by asking an LLM to create an answer</strong>. You need to find an actual data set or else collect new data.</p>
<hr />
<h2 id="heading-synthetic-data-fails-empirically-yet-thats-not-the-right-question">Synthetic Data Fails Empirically (yet that’s not the right question)</h2>
<p>There have been various <em>empirical</em> studies assessing whether LLM synthetic data aligns with real data. I believe that is somewhat of a wrong question, which I’ll explain later. Meanwhile, <strong>empirical results contradict the claim that synthetic data is similar to human data</strong>. Here are a few examples.</p>
<ul>
<li><p>Bisbee et al (2024) demonstrated that <strong>ChatGPT survey results are unstable and are not representative of human survey answers</strong>. They found that, “sampling by ChatGPT is not reliable for statistical inference … [Also] the distribution of synthetic responses varies with minor changes in prompt wording, and … the same prompt yields significantly different results over a 3-month period.”</p>
</li>
<li><p>Paxton &amp; Yang (2024) found that <strong>LLMs and humans report strongly differing “attitudes” about technology products</strong>. They found that “language model responses diverge from human responses—often dramatically … [These] divergent results are robust to multiple prompt variations, model families (Gemini, GPT, etc.), and major updates to the models … [Therefore] language model responses should not be used to replace or augment human survey responses at this point in time.”</p>
</li>
</ul>
<p>The following table is one snapshot from Paxton &amp; Yang, assessing the correlations in attitudinal ratings obtained from ChatGPT, Google Gemini, and human raters. Among 8 attitudes, it was only for ratings of “helpfulness” that the correlation exceeds <em>r</em>\=0.20; and even then, it was only <em>r</em>\=0.28-0.30, a weak correlation. The median correlation between LLMs and human attitudes was <em>r</em>\=0.10. This means that <strong>LLMs reproduced a median of only 1% (r²) of the pattern of attitudinal ratings of human respondents</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749685971994/ef04646f-0974-4a94-a4ac-a4f204fdb0a5.png" alt="A correlation matrix from Paxton &amp; Yang, 2024, comparing the emotional valence of ratings obtained from human respondents, Gemini LLM, and ChatGPT LLM. Across all 8 dimensions, the correlation between LLM and human ratings was low, in no case exceeding r = 0.3, and with a median agreement of r=0.10. By contrast, the two LLM models had higher agreement with one another (median r=0.19 but high variance, ranging r=-0.04-0.98)." class="image--center mx-auto" /></p>
<ul>
<li>Samoylov (2024) noted multiple problems with LLM-created data, especially that <strong>LLM results vary dramatically, across many dimensions, in response to relatively simple rephrasing of prompts</strong>. Similar to my point above, he further noted that it is impossible to know when one’s domain of interest is covered by LLM training data. He wrote, “<strong>how [a prompt] was worded massively affected the results. This is a demonstration of the test-retest unreliability of using LLM-generated responses</strong> … because you do not know what most of the LLMs were trained on, you do not know what kind of knowledge they encode … this one observation is enough to make anyone interested in getting real data look the other way”.</li>
</ul>
<blockquote>
<p>An especially nice feature of Samoylov’s article is the inclusion of <strong>R code to demonstrate the unreliability of LLM synthetic data</strong>. You can update his prompts and examine for yourself whether, in your domain, the answers from an LLM are reliable. The code is at the end of <a target="_blank" href="https://conjointly.com/blog/synthetic-respondents-are-the-homeopathy-of-market-research/">his article</a>.</p>
</blockquote>
<p>In short, these <strong>empirical studies demonstrate that</strong>:</p>
<ul>
<li><p><strong>Results vary from LLM model to model, time to time, and prompt to prompt</strong>.<br />  <em>Implication</em>: synthetic data are not reliable in the way we expect data to be reliable.</p>
</li>
<li><p><strong>Results do not agree with human responses</strong> or the patterns of human responses.<br />  <em>Implication</em>: synthetic data do not have construct validity to mimic human responses in the way they claim. (<a target="_blank" href="https://quantuxblog.com/convergent-and-discriminant-validity">Read more about construct validity here</a>.)</p>
</li>
</ul>
<p>In the post so far, we’ve seen: (1) that <strong>the concept of synthetic data misunderstands how surveys work</strong> as motivated communication so the data don’t make sense logically; and, (2) even if we set that problem aside, the <strong>resulting data empirically do not align with human survey data</strong>. Next, I’ll examine why empirical claims about synthetic data are unscientific in any case.</p>
<hr />
<h2 id="heading-yet-empirical-evaluation-is-not-the-right-question">… Yet empirical evaluation is not the right question</h2>
<p><strong><em>[tl/dr; This is a long sub-section, but I hope it will be worth your time to consider the logic here!]</em></strong></p>
<p>Although the empirical work above is admirable, it reflects a <strong>whack-a-mole strategy</strong>. Empirical investigations have no particular theoretical justification; instead, they respond to claims by AI proponents. Such proponents claim synthetic data “works” and demand proof otherwise.</p>
<p><em>Let’s assume the opposite of the previous empirical results for a moment. Let’s suppose — for the sake of argument — that synthetic responses to survey items</em> <strong><em>are</em></strong> <em>expected to be universally reliable and valid.</em> What would that mean? It would imply that an LLM could, in principle, be expected to answer any question, on any survey, similarly to how a human would answer.</p>
<p>To make that more explicit, let’s look again at the general claims for synthetic data. Suppose we want to know about the overall population likelihood that people will purchase our new product. A traditional survey of purchasing intent will:</p>
<ul>
<li><p><strong>Target a group</strong> of people and ask about each person’s <strong>intention</strong> to purchase our product</p>
</li>
<li><p>Expect that the intention will be somewhat <strong>indicative of future behavior</strong></p>
</li>
<li><p>Sample a population so we can <strong>estimate the aggregate behavior</strong> of the targeted group</p>
</li>
</ul>
<p>The claim for synthetic data is the same: <strong>that we can use synthetic responses to “sample” a group and estimate its aggregate intentions or behavior</strong>. For example, we might estimate whether synthetic “purchasers” are likely to purchase our product. And then we project that estimate to humans.</p>
<p>When we think about that for a moment, we might notice two things. First, <strong>it seems highly implausible.</strong> If a data provider has an LLM with such capability — to credibly answer any survey question similarly to humans would answer, about future behavior — why would they use it to sell survey panel responses?</p>
<p>Imagine, as one example, that we can accurately assess the intentions of executives and stock traders. For instance, we could envision survey items about their intended behaviors, such as these:</p>
<ul>
<li><p>Tomorrow, will you sell XYZ stock?</p>
</li>
<li><p>Today, will you increase your hedge of ABC currency?</p>
</li>
<li><p>In the next hour, will you short UVW stock?</p>
</li>
</ul>
<p>Just to be clear, <strong>these questions do not ask for <em>predictions</em> about future stock prices</strong> or the like. They ask only about a person’s <strong><em>intended behavior</em></strong>. In exactly the same way we might survey consumers about intentions to purchase a product, we could ask traders about intentions to purchase stocks, commodities, or currency. We can use the aggregate of those responses to infer likely population behavior.</p>
<p>If we could accurately assess such behavioral intentions, we could use that aggregate information quite profitably to predict stock, currency, or commodity futures — and <strong>that would be a much better business than selling survey responses</strong>. It is easy to imagine similar examples of valuable data in the domains of politics, medicine, pharmaceuticals, military affairs, logistics, shipping, and the like. If we could use synthetic data to estimate intended behaviors of groups of people in those domains, it would have value that far exceeds that of selling survey panel data.</p>
<blockquote>
<p><strong>“But wait!” : Side note about such a hypothetical survey</strong></p>
<p>Let’s set aside LLMs for a moment. Do you object that executives and traders would not answer such items on a survey? That they would not report their intended trades? Or that they would not answer honestly? Do you worry that any answers they give wouldn’t predict real behavior?</p>
<p>Good! If you think that, your views may align with interpreting surveys as being about motivated communication, and not as about sampling any particular “reality” apart from that. And that implies that the core premise of synthetic data is illogical, as I noted at the outset of this post (you might return to the top of this post and re-read my discussion; or see <a target="_blank" href="https://quantuxblog.com/surveys-and-the-true-score-mistake">this separate post</a> for more.)</p>
</blockquote>
<p>For this first point, my inference is this: <strong>synthetic data providers do not believe that their systems can do anything like this</strong> or that their data have such value. Instead, they sell such data <em>because</em> it has low value. Besides their own behavior, they reveal their belief in the value of such through their pricing, and in their statements about how cheap it is.</p>
<blockquote>
<p>Now, maybe you’re thinking, “I understand the argument, but perhaps providers do use the LLMs profitably in the way you describe, but they do so secretly while they also sell survey responses as a second business.” That is logically possible, yet: (1) why would they give away a competitive advantage of that kind and let others match what they are doing? (2) where are the case studies to prove such capability ? (3) why is their pricing so low? Overall, this possibility seems extremely unlikely to be the case, for all the reasons outlined here.</p>
</blockquote>
<p>Second, the claim that any survey question might credibly be answered by an LLM is <strong>not a claim that can be tested empirically</strong>. How would one go about testing that? One would need to define the space of all possible survey questions, determine a sampling strategy for that space, and determine a way to find the “ground truth” of human responses across that entire space to compare to LLM responses. And would need to show that the infinity of possible spaces maps to a majority of its constituent subspaces.</p>
<p>I have no idea how one could do that in a general way. Therefore, the question of empirically assessing LLMs is necessarily limited to specific domains (as in each of the examples above) and cannot be evaluated generally.</p>
<p>Consider, third, that when we evaluating the reliability of synthetic data in any particular domain, an LLM proponent could always claim “that is just one example.” This shows that <strong>an empirical assessment strategy cannot answer the general claim</strong> about answering survey items, because that general claim is not an empirical claim. Instead, it is a belief disguised as a claim.</p>
<p>In summary, <strong>the “hypothesis” that is supposedly being tested in empirical evaluation of synthetic data — namely, that it can replace human data — is a <em>marketing</em> <em>claim</em> and not a scientific hypothesis</strong>.</p>
<p>Next we’ll look at another marketing claim: that synthetic data can replace hard-to-reach subgroups of respondents.</p>
<hr />
<h2 id="heading-synthetic-data-cannot-overcome-sampling-limitations">Synthetic Data Cannot Overcome Sampling Limitations</h2>
<p>LLM-generated data <strong>providers postulate that synthetic data can emulate “hard to reach audiences”</strong> (<a target="_blank" href="https://www.qualtrics.com/edge/">example</a>). The term “hard to reach” is rarely defined by providers. Instead, they use vague references to imprecise notions such as “increasing the diversity” of respondents.</p>
<p>From discussions I’ve had, this is often interpreted in two ways. First, it may mean <em>niche audiences</em> who have low prevalence in panels (corporate executives, physicians, developers, users of one specific product, etc). Second, it may mean historically <em>marginalized respondents</em> such as ethnic minority groups, people with disabilities, different language groups, and others.</p>
<p>In either case, the premise is that <strong>these are people who do not answer our surveys as often as we researchers would like them to</strong>. (<em>Remember my argument that surveys reflect motivated communication? This premise also aligns with that view</em>.) Platforms claim that they can “boost” the responses for such people using synthetic data that either supplements or completely replaces real data.</p>
<p>Unfortunately, <strong>there is no reason to believe that LLMs can accomplish the goal of representing difficult-to-reach audiences, and there are several reasons to expect otherwise</strong>:</p>
<ul>
<li><p>We saw above that LLM data does not agree with general population responses. <strong>Why would an LLM give better data for subgroups</strong> than it does for larger populations? There is no logical or statistical reason to expect that.</p>
</li>
<li><p>We know that LLM training data over-represents some groups (English speaking, white, educated, western, technology-interested, affluent) and under-represents other groups (non-English speaking, other than white, non-western, etc.). There are algorithmic approaches to reduce bias, but — even if a provider has implemented some of them — <strong>how would we know that bias has been eliminated for some potentially unique group of interest that we want to sample?</strong> There is no a priori bias reduction that works in advance, for targeted samples that will only be specified later. (This is similar to the empirical questions above; it cannot be answered in a general way but only on a per-domain basis.)</p>
</li>
<li><p>Even when subgroup bias has been addressed in some way, the approaches are fragile and non-generalizable. Ferrara (2024) has written of this as the “butterfly effect,” that <strong>small changes to algorithms, training data, or prompts can lead to substantial changes in the output of LLM models that magnify the effect of biases</strong>. What does that imply? Even if an LLM system generates reliable synthetic data that overcomes biases at one point in time and for one group, we have no reason to expect that it will do so at a later time, or with a different group, or after an algorithm update.</p>
</li>
</ul>
<p>In short, <strong>the claim that LLM synthetic data can amplify otherwise underrepresented groups appears to be highly unlikely as a statistical matter, and is impossible to prove</strong> as a general expectation.</p>
<p>There is another sampling problem that I’ve <a target="_blank" href="https://quantuxblog.com/research-concerns-for-llm-applications">written about separately</a>: the basic concepts and statistics of sampling do not apply to LLMs. This is additional to the concerns in this post, and it means that, besides the concerns here, <strong>any “sample” from an LLM has no particular statistical meaning</strong>. It is, in fact, not a sample at all but output of a mostly uncharacterized stochastic process. (This implies, for example, that we cannot place meaningful confidence intervals on statistics from synthetic data; the estimates certainly are <em>not</em> exact values, yet we have no way to assess our degree of sampling certainty about them.)</p>
<hr />
<h2 id="heading-side-note-questions-for-research-ethics">Side Note: Questions for Research Ethics</h2>
<p>Although I don’t have space here to consider all aspects of research ethics, there a core question that I encourage every researcher to ask: does synthetic data meet ethical obligations to society and ourselves?</p>
<p>Various governmental and professional organizations outline ethical requirements for research. These include legal definitions such the US Code of Federal Regulations (<a target="_blank" href="https://www.ecfr.gov/current/title-42/section-93.234">42 CFR 93.234</a>) and professional standards such as the <a target="_blank" href="https://www.apa.org/ethics/code">Ethics Code</a> of the American Psychological Association (2017). The legal definition in US 42 CFR is relatively typical:</p>
<blockquote>
<p>Research misconduct means fabrication, falsification, or plagiarism in proposing, performing, or reviewing research, or in reporting research results.</p>
</blockquote>
<p>So the question is: <strong>are synthetic data fabricated, falsified, or plagiarized</strong>?</p>
<p>I don’t propose an answer here because the set of considerations — from models of ethics such as deontology vs. consequentialism, to the definition of individual words and the functioning of specific LLM systems with respect to plagiarism (e.g., <a target="_blank" href="https://www.theatlantic.com/technology/archive/2025/03/search-libgen-data-set/682094/">Reisner, 2025</a>) — is too large to tackle in this post.</p>
<p>Instead, I believe that the question is important (and sometimes mandated) for each of us to consider and answer for ourselves. We should be confident that our research aligns with ethical requirements!</p>
<hr />
<h2 id="heading-other-uses-for-synthetic-data-unfortunately-unconvincing">Other Uses for Synthetic Data: Unfortunately Unconvincing</h2>
<p>Here are four common claims for synthetic data other than reporting it, with my brief rebuttals.</p>
<ul>
<li><p><strong>Synthetic data accelerates research</strong>. <em>Rebuttal</em>: synthetic data is not data; using it is not research.</p>
</li>
<li><p><strong>Synthetic data can pre-test a survey and analyses</strong>. <em>Rebuttal</em>: random responses, as many platforms provide, are preferable. Random responses are free of assumptions and correlated patterns, giving more comprehensive and unbiased tests. Random data can also be used to <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-4-inspecting-model-fit-with-rlh">help assess data validity</a>.</p>
</li>
<li><p><strong>Synthetic data can preview expected results with stakeholders</strong>. <em>Rebuttal</em>: such data doesn’t preview anything. Better — and much less risky — is for a researcher to use a combination of domain knowledge and stakeholder engagement to create a few scenarios reflecting potential outcomes. We can discuss those with high specificity and relevance, without fabricating data.</p>
</li>
<li><p><strong>Other colleagues will use such data, but I can use it more carefully</strong>. <em>Rebuttal</em>: they shouldn’t use it, either. We can’t let research ethics and practice be defined by what non-researchers might do.</p>
</li>
</ul>
<p>Overall, <strong>I don’t view any of these claims as a good reason to use synthetic data</strong>. Let’s consider the benefits and risks. On the benefits side: synthetic data shows low value and there are preferable alternatives. On the risks side: creating synthetic data could lead to it being accidentally used, reported, or demanded by executives. In my opinion, the low benefits are strongly outweighed by the substantial risk.</p>
<p>Although it is great to pre-test a survey and its analyses, and it’s also great to preview potential results with stakeholders, those goals do not require synthetic data and are accomplished better without it.</p>
<hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>My personal conclusion — as a matter of logic, empirical findings, statistical reasoning, and scientific principles — is that <strong>synthetic data has no place in survey research</strong>. I also believe the purported use cases for synthetic data are unconvincing; alternative approaches are superior in their results and are less risky in practice. And there are important questions about research ethics that each of us should consider.</p>
<blockquote>
<p>Disagree? Publish your reasoning and results! (For a venue, check out <a target="_blank" href="https://quantuxcon.org">quantuxcon.org</a>)</p>
</blockquote>
<p>All of this poses a question: why is there so much interest in synthetic data? Samoylov (2024) argues that it reflects a “snake oil” industry that is intent to sell products to naive customers.</p>
<p>For my part, <strong>I see the hope placed in synthetic data as a form of magical thinking</strong>. It is certainly appealing to believe that a data genie can magically create the data I need! And it is even more appealing to believe that it can free me from the difficulties of collecting real data, while getting results faster.</p>
<p>But more likely, we can’t escape the real work of survey research: collecting good data that informs unique decisions. On a happy note, <strong>actual research — which is to say, learning from people — is not only informative about the real world, it is also an enjoyable and rewarding enterprise</strong>.</p>
<p>And that process of learning from people will always deliver enough value to exist!</p>
<hr />
<h2 id="heading-references">References</h2>
<p>American Psychological Association (2017). Ethical Principles of Psychologists and Code of Conduct. At <a target="_blank" href="https://www.apa.org/ethics/code">https://www.apa.org/ethics/code</a></p>
<p>Bisbee J, Clinton JD, Dorff C, Kenkel B, Larson JM. Synthetic Replacements for Human Survey Data? The Perils of Large Language Models. <em>Political Analysis</em>. 2024; 32(4):401-416. doi:10.1017/pan.2024.5</p>
<p>Ferrara E (2024). The Butterfly Effect in artificial intelligence systems: Implications for AI bias and fairness. <em>Machine Learning with Applications</em>, Volume 15. <a target="_blank" href="https://doi.org/10.1016/j.mlwa.2024.100525">https://doi.org/10.1016/j.mlwa.2024.100525</a>. At <a target="_blank" href="https://www.sciencedirect.com/science/article/pii/S266682702400001X">https://www.sciencedirect.com/science/article/pii/S266682702400001X</a>.</p>
<p>Paxton J, Yang Y. (2024). “Do LLMs simulate human attitudes about technology products?” In <em>Proceedings of the 2024 Quantitative User Experience Conference</em>. At <a target="_blank" href="https://drive.google.com/file/d/16F_JZv4eHNiDMJT6BT7F6m97C2rBX8-7/view?usp=sharing">https://drive.google.com/file/d/16F_JZv4eHNiDMJT6BT7F6m97C2rBX8-7/view?usp=sharing</a></p>
<p>Reisner A (2025). “Search LibGen, the Pirated-Books Database That Meta Used to Train AI”. <em>The Atlantic</em>, at <a target="_blank" href="https://www.theatlantic.com/technology/archive/2025/03/search-libgen-data-set/682094/">https://www.theatlantic.com/technology/archive/2025/03/search-libgen-data-set/682094/</a></p>
<p>Samoylov N (2024). Synthetic respondents are the homoeopathy of market research. At <a target="_blank" href="https://conjointly.com/blog/synthetic-respondents-are-the-homeopathy-of-market-research/">https://conjointly.com/blog/synthetic-respondents-are-the-homeopathy-of-market-research/</a></p>
<p>US Code of Federal Regulations (2024). Research misconduct. 42 CFR 93.234. <a target="_blank" href="https://www.ecfr.gov/current/title-42/section-93.234">https://www.ecfr.gov/current/title-42/section-93.234</a></p>
<hr />
<p><em>Finally</em>, as always, this post was …</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749920743009/ba0286e7-fcad-403c-b6af-68878cb6b090.png" alt="written by a human, not by AI" class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Community Scheduling: 
An Example of TURF Analysis]]></title><description><![CDATA[Today I’ll take a brief look at TURF analysis and show a simple example.
At a high level, TURF answers this question: after we do the #1 best thing — whether that is a product, message, placement, etc. — that customers desire most, what should we do ...]]></description><link>https://quantuxblog.com/community-scheduling-an-example-of-turf-analysis</link><guid isPermaLink="true">https://quantuxblog.com/community-scheduling-an-example-of-turf-analysis</guid><category><![CDATA[quantux]]></category><category><![CDATA[mrx]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Mon, 12 May 2025 22:54:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746910061344/3fc0b335-4837-443b-9751-ab7d2a8d9b7f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Today I’ll take a brief look at TURF analysis and show a simple example.</p>
<p>At a high level, TURF answers this question: after we do the #1 best thing — whether that is a product, message, placement, etc. — that customers desire most, what should we do next to reach <em>additional</em> customers, beyond those who want the #1 thing? It is not necessarily the #2 thing from our list.</p>
<p><strong>TURF</strong> stands for “<strong>total unduplicated reach and frequency</strong>” (Miaoulis et al, 1990). That means that it find combinations of items that will <em>reach</em> the largest <em>total</em> number of people, while having as few <em>duplications</em> [multiple exposures per person] as possible . And, secondarily, it will maximize the <em>frequency</em> — average number of reaches per person — <em>after</em> reaching the most unique people.</p>
<blockquote>
<p>A classic TURF example is to reach as many unique viewers as possible with advertisements. If you ignore the question of pricing, you would want first of all to place an ad into the most popular channel (TV show, magazine, etc.) That would have the largest audience (“reach”) for a single placement. But after that placement, what is the best second choice? Is it the 2nd most popular channel? Not necessarily, because the second most popular channel may have high overlap with the first channel. For instance, visitors to the most popular websites also tend to visit many of the most popular websites; they are heavy internet users across many sites.</p>
<p>If you just go by ranked popularity, you would often be advertising to — reaching — the same people again. It may be better to go farther down the list of popularity and find a channel that is smaller but that reaches a unique audience, relative to the #1 channel.</p>
</blockquote>
<p>My example is this: <strong>when scheduling practice times for an</strong> <a target="_blank" href="https://quantuxblog.com/announcing-tech-community-zen"><strong>online Zen group</strong></a><strong>, what is the best <em>set</em> of times that will reach the most people</strong>, so at least one time in the set will work for as many people as possible? Respondents took a survey and said when they are available. Given that, I want to find a small number of times that will make practice available to as many unique people as possible. TURF is a way to do that.</p>
<p>In this post, I share a data set — after protecting privacy as noted below — and R code for TURF analysis. In this data set, it would be possible to do TURF by hand. However, it is a great example for R analysis, and would easily scale to larger data sets. As always, I share R code along the way and compile it at the end.</p>
<hr />
<h2 id="heading-the-data">The Data</h2>
<p>I fielded a <a target="_blank" href="https://surveys.sawtoothsoftware.com/67c8902733613c9ad1b9818e">survey</a> that asks respondents what times they are available on weekdays. Each respondent answered for their local time zone, which was later converted to standard time. (BTW, I also asked about <em>weekends</em>, and preferred <em>days</em> of the week. But for this post, we’ll only look at weekday times.) The results are a grid of availability by respondent, adjusted for time zone, mapped to 24 hours of the day.</p>
<p>Unlike many of my surveys, these data are private. However, I have created <strong>a “permuted,” simulated data set</strong> that I can share. It preserves enough high-level characteristics to give an identical TURF answer.</p>
<blockquote>
<p><strong>More detail</strong>. In these data, using random permutation of rows and columns, no observation is identical to the original data, and all sets of individual answers — and the relationships among times for every person — are altered randomly. This way, the data set preserves privacy while also reproducing the overall counts of availability by row and column. It gives identical TURF answers as my real data. The permuted data set was generated by the R <code>vegan::permatswap()</code> function (Oksanen et al, 2025).</p>
<p>Why don’t I just use fake data? Because this way, I can share results with the Zen community as well as the Quant community, and show exactly how the schedule was decided.</p>
</blockquote>
<p>The permuted data set is small enough to share it in code, with no need to download it. I start with output obtained from the R <code>dput()</code> function, giving the data in text format. To create our data set <code>hourgrid</code>:</p>
<pre><code class="lang-r"><span class="hljs-comment"># the following was obtained from R dput() after survey import, data permutation, etc.</span>
hourgrid &lt;- structure(c(<span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>), 
  dim = c(<span class="hljs-number">39L</span>, <span class="hljs-number">24L</span>), 
  dimnames = list(
    c(<span class="hljs-string">"1"</span>, <span class="hljs-string">"2"</span>, <span class="hljs-string">"3"</span>, <span class="hljs-string">"4"</span>, <span class="hljs-string">"5"</span>, <span class="hljs-string">"6"</span>, <span class="hljs-string">"7"</span>, <span class="hljs-string">"8"</span>, 
      <span class="hljs-string">"9"</span>, <span class="hljs-string">"10"</span>, <span class="hljs-string">"11"</span>, <span class="hljs-string">"12"</span>, <span class="hljs-string">"13"</span>, <span class="hljs-string">"14"</span>, <span class="hljs-string">"15"</span>, <span class="hljs-string">"16"</span>, <span class="hljs-string">"17"</span>, <span class="hljs-string">"18"</span>, <span class="hljs-string">"19"</span>, 
      <span class="hljs-string">"20"</span>, <span class="hljs-string">"21"</span>, <span class="hljs-string">"22"</span>, <span class="hljs-string">"23"</span>, <span class="hljs-string">"24"</span>, <span class="hljs-string">"25"</span>, <span class="hljs-string">"26"</span>, <span class="hljs-string">"27"</span>, <span class="hljs-string">"28"</span>, <span class="hljs-string">"29"</span>, <span class="hljs-string">"30"</span>, 
      <span class="hljs-string">"31"</span>, <span class="hljs-string">"32"</span>, <span class="hljs-string">"33"</span>, <span class="hljs-string">"34"</span>, <span class="hljs-string">"35"</span>, <span class="hljs-string">"36"</span>, <span class="hljs-string">"37"</span>, <span class="hljs-string">"38"</span>, <span class="hljs-string">"39"</span>), 
    c(<span class="hljs-string">"1:00"</span>, <span class="hljs-string">"2:00"</span>, <span class="hljs-string">"3:00"</span>, <span class="hljs-string">"4:00"</span>, <span class="hljs-string">"5:00"</span>, <span class="hljs-string">"6:00"</span>, <span class="hljs-string">"7:00"</span>, <span class="hljs-string">"8:00"</span>, <span class="hljs-string">"9:00"</span>, 
      <span class="hljs-string">"10:00"</span>, <span class="hljs-string">"11:00"</span>, <span class="hljs-string">"12:00"</span>, <span class="hljs-string">"13:00"</span>, <span class="hljs-string">"14:00"</span>, <span class="hljs-string">"15:00"</span>, <span class="hljs-string">"16:00"</span>, 
      <span class="hljs-string">"17:00"</span>, <span class="hljs-string">"18:00"</span>, <span class="hljs-string">"19:00"</span>, <span class="hljs-string">"20:00"</span>, <span class="hljs-string">"21:00"</span>, <span class="hljs-string">"22:00"</span>, <span class="hljs-string">"23:00"</span>, <span class="hljs-string">"0:00"</span>)
    )
  )
</code></pre>
<p>Next I structure it as a <code>data.frame</code> and clean it up a bit:</p>
<pre><code class="lang-r"><span class="hljs-comment"># set that up as a nice dataframe and keep colnames as "hours"</span>
tmp.names       &lt;- colnames(hourgrid)   <span class="hljs-comment"># save the names instead of what data.frame() does</span>
hourgrid        &lt;- data.frame(hourgrid)
names(hourgrid) &lt;- tmp.names     
hourgrid$ID     &lt;- <span class="hljs-number">1</span>:nrow(hourgrid) <span class="hljs-comment"># add ID variable needed later by TURF</span>
<span class="hljs-comment"># check its structure</span>
head(hourgrid)
</code></pre>
<p>With the <code>head()</code> command, we see that each row shows a person’s availability for each hour of the day. It is coded as 1 if they said they are available at that time. (as noted, it asked for responses in the local time for each respondent. Those were standardized into Pacific time before this analysis started.)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746911828498/cb85075a-bf5c-409d-b10b-169a95838066.png" alt class="image--center mx-auto" /></p>
<p>We can find the most popular times by summing the 24 columns representing each hour:</p>
<pre><code class="lang-r"><span class="hljs-comment"># count of preferred times (omitting ID column)</span>
colSums(hourgrid[ , <span class="hljs-number">1</span>:<span class="hljs-number">24</span>])
</code></pre>
<p>We see that 09:00 and 15:00 (not shown here) are the most popular times, followed by 10:00 and 12:00:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746912062081/87e09cd3-7a05-4ead-b271-65d4b5eefc88.png" alt class="image--center mx-auto" /></p>
<p>Even better is a data visualization. For this data set, we can do a <strong>heatmap</strong> of times by respondent. Here’s R code for that:</p>
<pre><code class="lang-r"><span class="hljs-keyword">library</span>(ggplot2)
<span class="hljs-keyword">library</span>(reshape2)
<span class="hljs-comment"># melt the data for nice ggplot structure</span>
hours.m &lt;- melt(subset(hourgrid, rowSums(hourgrid[ , <span class="hljs-number">1</span>:<span class="hljs-number">24</span>]) &gt; <span class="hljs-number">0</span>), 
                id.vars = <span class="hljs-string">"ID"</span>)
names(hours.m) &lt;- c(<span class="hljs-string">"Respondent"</span>, <span class="hljs-string">"Time"</span>, <span class="hljs-string">"Available"</span>)

<span class="hljs-comment"># plot it</span>
p &lt;- ggplot(data=hours.m, 
            aes(x=Respondent, y=Time, fill=Available)) +
  geom_tile(color = <span class="hljs-string">"grey90"</span>,
            lwd = <span class="hljs-number">0.5</span>,
            linetype = <span class="hljs-number">1</span>) +
  scale_fill_gradient(low = <span class="hljs-string">"white"</span>, high = <span class="hljs-string">"darkblue"</span>) +
  scale_y_discrete(limits = rev(levels(hours.m$Time))) +
  coord_fixed() +
  theme_minimal() +
  theme(panel.grid.major = element_blank(), panel.grid.minor = element_blank()) +
  theme(legend.position = <span class="hljs-string">"none"</span>) +
  xlab(<span class="hljs-string">"Respondent (permuted data)"</span>) +
  ylab(<span class="hljs-string">"Times Selected by Respondent (permuted data)"</span>)

p
</code></pre>
<p>Two small notes on that code. First, when <code>melt()</code>‘ing the data, I keep only respondents with <strong>at least 1 time</strong> selected (<em>nb</em>, in <code>ggplot()</code> this can lead to blank columns; solve by making ID an ordered factor, or create a new ID without gaps). Second, I <strong>reverse the Y axis</strong> using <code>scale_y_discrete(limits = rev(levels(…)))</code> so the times are shown in a more natural order. Here’s the plot:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746912440219/4ff28009-068f-4141-84e8-27d678f894ae.png" alt class="image--center mx-auto" /></p>
<p>We can see a few characteristics of the data. First of all — remembering that these are permuted data with the same high level structure but not people’s actual individual responses — <strong>most respondents only selected a few times</strong> they are available, commonly 1, 2, or 3 times on a given day. Second, we see that the times are distributed across almost all hours of the day (which reflects the worldwide community of respondents). Thus, <strong>the overall grid is relatively sparse</strong>.</p>
<p>FWIW, we can calculate the sparseness. Add up all the selected times and divide by the size of the grid:</p>
<pre><code class="lang-r"><span class="hljs-comment"># how sparse is it?</span>
sum(hourgrid[ , <span class="hljs-number">1</span>:<span class="hljs-number">24</span>]) / nrow(hourgrid) / <span class="hljs-number">24</span>
</code></pre>
<p>The answer is <code>0.108</code>, i.e., the “average” respondent selected 10.8% of the available times.</p>
<p>All of this tells us that the single most popular time would be either 09:00 or 15:00 Pacific. But <strong>what if we want to schedule 2 or 3 times that maximize availability for the largest number of people</strong>, where each of them has at least one time that would work? <strong>That’s where TURF comes in</strong>.</p>
<blockquote>
<p><strong>What about days of the week?</strong> For purposes here, I’m ignoring that. This grid has responses for weekdays (weekends were asked separately). Among all 7 days of the week, the most popular days were Monday and Tuesday. Of those, only Tuesday works for me to schedule the meetings. Thus, in this post, I assume that we’re talking about times on Tuesdays.</p>
<p>BTW, you might wonder: why not ask about all times, across all days using a 7 day x 24 hour grid? Answer: in that format, respondents tend to select only a small number and data are extremely sparse. However, we could still use TURF with data of that kind.</p>
</blockquote>
<hr />
<h2 id="heading-turf-code-and-results">TURF Code and Results</h2>
<p>It is relatively straightforward to write one’s own TURF function, but we don’t need to; there is already an easy-to-use <code>turfR</code> library for R (Horne, 2014). First, we slightly reformat the data to match its expectation:</p>
<pre><code class="lang-r"><span class="hljs-comment"># TURF Analysis</span>
<span class="hljs-keyword">library</span>(turfR)

<span class="hljs-comment"># set up the data for TURF package</span>
<span class="hljs-comment"># add ID variable so respondents are identified to TURF</span>
turf.dat &lt;- data.frame(
  ID = hourgrid[ , <span class="hljs-string">"ID"</span>],           <span class="hljs-comment"># respondent ids</span>
  Weight = rep(<span class="hljs-number">1</span>, nrow(hourgrid)),  <span class="hljs-comment"># the weight / importance for each respondent</span>
  hourgrid[, <span class="hljs-number">1</span>:(ncol(hourgrid)-<span class="hljs-number">1</span>)]  <span class="hljs-comment"># columns of data to use; all except "ID"</span>
)
head(turf.dat)
</code></pre>
<p>This format is almost identical to our original format, except that it adds a <code>Weight</code> for each respondent. The <em>weight</em> is the “importance” to give each observation. For example, if you are working with a customer database, you might want to weight customers according to their observed revenue, longevity, retention, segment membership or the like; or use similar estimated from a regression model; or from responses to a survey item such as their level stated interest. For our data, I assign equal <strong>weight</strong> of 1 to all observations.</p>
<p>Here’s what the <code>turfR</code> data object looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746915956498/67aa8f99-6000-49a0-8124-478d279789d1.png" alt class="image--center mx-auto" /></p>
<p>Although our data has simple values of 1 and 0 for people’s availability, <strong>binary data is not required for TURF</strong>. TURF can work with continuous values (see note at the end), such as likelihood scores from a database, regression model, or simple Likert responses; estimated preference values (e.g., from a choice model survey); and so forth. In our case, the data come from checkboxes of availability on a survey, so 1/0 is the natural format.</p>
<p>With that, <strong>we can now fit a TURF model</strong>. I’ll start by looking for the best 2 times (<code>k=2</code>) and ask for the top 10 results (<code>keep=10</code>). We tell it that we have 24 items (times) to consider (<code>n=24</code>). Here’s the code and result:</p>
<pre><code class="lang-r"><span class="hljs-comment"># run the TURF analysis for best 2 times</span>
(turf.2 &lt;- turf(turf.dat, n=<span class="hljs-number">24</span>, k=<span class="hljs-number">2</span>, keep=<span class="hljs-number">10</span>))$turf
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746916003151/dc0e7a4a-b4e0-4016-9ca0-f314edf41431.png" alt class="image--center mx-auto" /></p>
<p>I’ll interpret results in the next section, but will note here an <strong><em>unusual but happy coincidence</em></strong> in these data: <strong>the item &amp; column numbers (1—24) correspond exactly to the hours of the day</strong> (01:00—24:00/0:00). That makes the results easy to read, but it is a unique situation for this data set and question. In most cases, you would have to match those numbers to interpretable names of items, features, messages, etc.</p>
<p>For my purposes, I’ll use TURF results for the tentative 3 best times to schedule. Here’s how to get those, asking for the 3 best times (<code>k=3</code>) and keeping the top 15 results (<code>keep=15</code>):</p>
<pre><code class="lang-r"><span class="hljs-comment"># what if we scheduled 3 times instead of 2? </span>
(turf.3 &lt;- turf(turf.dat, n=<span class="hljs-number">24</span>, k=<span class="hljs-number">3</span>, keep=<span class="hljs-number">15</span>))$turf
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746916051182/3d2c26f6-ca33-40a3-99cb-f33489759b97.png" alt class="image--center mx-auto" /></p>
<p>BTW, in this simple case, the algorithm does a <em>full expansion</em> to consider <strong>all possible combinations</strong>. If we had larger sets to consider, that would become computationally intractable (e.g., choosing the best sets of 20 ads to rotate out of a set of 100 options). In such cases, <code>turfR</code> offers <strong>Monte Carlo sampling</strong> options.</p>
<hr />
<h2 id="heading-selecting-among-the-results">Selecting Among the Results</h2>
<p>For my scheduling problem, I will start by scheduling meetings 1 time per week and then soon expand to 2 and eventually perhaps 3 times per week. Given that plan (i.e., “strategic goal”), I’ll use the <code>k=3</code> results for the optimal 3 meetings per week, and develop the schedule in light of those results.</p>
<p>Following is the <code>k=3</code> result again (first 6 solutions). Before jumping into the schedule choice, I’ll explain the first 3 columns: <code>combo</code>, <code>rchX</code>, and <code>frqX</code>. The <em>combo</em> is simply the internal solution number that <code>turfR</code> considered. We can ignore that unless we want to dig deeply into its solution design matrix.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747069403432/81b77386-0365-4f69-b5e1-9c2c447dedad.png" alt class="image--center mx-auto" /></p>
<p><strong>The next two columns,</strong> <code>rchX</code> <strong>and</strong> <code>frqX</code><strong>, are the heart of the TURF solution</strong>: the <em>reach</em> (<code>rchX</code>) and <em>frequency</em> (<code>frqX</code>) that the solutions reach, sorted in decreasing order of reach. Specifically, <code>rchX</code> is the proportion of respondents who are reached at least once by a given solution, which <code>frqX</code> is how often the average respondent is reached.</p>
<p>Consider the first solution, which is to offer meeting times at 09:00, 12:00, and 18:00 Pacific Time. That set of times is estimated to <em>reach</em> 69% of the respondents — meaning that at least 1 of those times would work for 69% of people who replied to the survey. However, some people are available for multiple times within that set. The <em>frequency</em> estimate tells us that this set of 3 times would offer 0.74 available meeting times per week to the average respondent. I.e., some of the 69% would have multiple options.</p>
<p>You might wonder, “<strong>what about confidence intervals (CIs)?</strong> Are there meaningful differences between 69% for the first solution and 67% for the second one?” Like anything involving CIs, it is a complex question. In this case, it is unclear to what extent we should consider these data to be a <em>sample</em> of a population — as a CI would assume — as opposed to being a descriptive <em>census</em> of a community. However, if we assume that it is a random sample, we could find CIs either with a bootstrap operation (best) or using a <strong>quick</strong> <strong>approximation for the binomial proportions</strong>. Given N=39 respondents and looking for an 80% confidence interval (±1.28 standard errors), we get a simple estimate of:</p>
<pre><code class="lang-r"><span class="hljs-comment"># est'd CI for the TURF #1 solution</span>
<span class="hljs-comment"># get the proportion reached for solution #1</span>
p.turf &lt;- turf.2$turf[[<span class="hljs-number">1</span>]]$rchX[<span class="hljs-number">1</span>]
<span class="hljs-comment"># estimate of 80% confidence interval (swag; bootstrap etc would be better)</span>
<span class="hljs-comment"># set the Z value (standard errors) as appropriate; in this case "80% CI" Z == 1.28</span>
ci     &lt;- <span class="hljs-number">1.28</span> * sqrt(p.turf * (<span class="hljs-number">1</span>-p.turf) / nrow(hourgrid))
c(p.turf - ci, p.turf + ci)
</code></pre>
<p>The answer is that our CI is a range of 0.598—0.787, meaning that solution #1 is estimated to reach approximately 60-79% of respondents. The other 14 solutions all fall within that range for reach. So, as a first approximation, none of those 14 are dramatically worse than solution #1. Here’s the CI computation using simple binomial approximation at the 80% level:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746981398957/140bfd13-22bf-4913-a8d4-a2cd4ffde71c.png" alt class="image--center mx-auto" /></p>
<p>With all of this in place, let’s look at the first 6 solutions. Then I’ll discuss how I chose among them with respect to overall goals. The solutions are:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746981590290/be1580ed-c6c7-43e5-802e-7f171a88ae7b.png" alt class="image--center mx-auto" /></p>
<p>Examining the patterns, a few things stand out. First, <strong>the 09:00 time appears in all 6 of the top results</strong>. Second, we see that solutions 2—5 are all <strong>tied for reach</strong> with one another, and are only slightly behind solution #1. Third, solutions 2 &amp; 3 — and to a lesser degree solutions 4 &amp; 5 — score somewhat <strong>higher on frequency</strong> than solution #1. Finally, some of the solutions including #1 and #5 have times <strong>spread out</strong> across the day (e.g., 09:00, 12:00, 18:00) whereas others are more compact, such as #4 that has times at 09:00, 10:00, and 12:00.</p>
<p><strong>Here’s how I assessed the possibilities and made a choice</strong> for the schedule.</p>
<ol>
<li><p><strong>For the first, initially scheduled time, 09:00 is a clear winner</strong>. Although it is slightly less popular on its own than 15:00 (N=12 vs. N=13), the 09:00 time appears in all of the top solutions. There it is a good choice of a foundational time, while allowing the schedule to evolve later.</p>
</li>
<li><p>Beyond that, I <strong>tentatively select 10:00 as the second, <em>expected</em> time slot</strong>. 10:00 appears in solutions #2 and #4 and thus is an excellent second choice. Additionally it has the advantage of being compact in time (assuming both sessions occur on the same day) which is logistically simple. It also appears in solutions with relatively higher frequency, meaning that it will give some flexibility to people.</p>
</li>
<li><p>If and when the community needs to have a <strong>third time scheduled</strong>, I’d want to see how 09:00 and 10:00 are going and then decide. Tentatively, 12:00 or 15:00 look promising; another option might be 18:00.</p>
</li>
</ol>
<p>This process of selecting 09:00 — while tentatively planning 10:00, and considering a more complete schedule over time — is a good example of a cautious process.</p>
<blockquote>
<p>At Amazon (and elsewhere) this is often described as the difference between a “two-way door” and a “one-way door”. A one-way door is a decision that is difficult to reverse. For example, announcing a scheduled time of 09:00 and scheduling practice then is difficult to reverse because people come to expect it. Similarly shipping a product, buying a house, and getting married are all one-way doors.</p>
<p>By contrast, a two-way door is relatively easier to reverse. Deciding tentatively on 10:00 as a planned next step is easy to change later. It is similarly “two way” and reversible to test a product prototype, rent an apartment, or to date someone as opposed to getting married.</p>
</blockquote>
<hr />
<h2 id="heading-a-few-applications-for-turf">A Few Applications for TURF</h2>
<p>In the worlds of Quant UX and marketing research, TURF analysis has many applications. Here are a few that occur to me somewhat randomly:</p>
<ul>
<li><p><strong>Constructing a restaurant or other “menu”</strong>: given preference data (such as a <a target="_blank" href="https://quantuxblog.com/easy-maxdiff-in-r">MaxDiff assessment</a>), which smallest set of items will often the most choices that appeal to as many customers as possible? (<em>Want to try it?</em> Get the Quant UX Association <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-1-data-averages">class preference data</a> and run TURF on the estimates!)</p>
</li>
<li><p><strong>Planning conference or workshop locations over time</strong>: which set of locations will maximize the appeal — on at least one occasion — for as many people as possible?</p>
</li>
<li><p><strong>Selecting marketing messages</strong>: if we can feature only a few messages, such as 3 claims on a product package or website, which set of messages will catch attention from as many people as possible?</p>
</li>
<li><p><strong>Offering employee perks or gifts</strong>: suppose we want a small number of things to offer to employees. Which set will be most likely to have “something for everyone”?</p>
</li>
<li><p><strong>Prioritizing customer outreach efforts</strong>: suppose our sales team wants to call customers who are highly interested in at least one of our new features. We want to select a limited number to call, prioritized jointly by feature interest <em>[reach]</em> and account size <em>[weight]</em>. Who are they?</p>
</li>
<li><p><strong>Pharmaceutical formulary</strong>: which combination of prescription drugs will offer the greatest coverage, to as many of our patients and/or physicians as possible?</p>
</li>
<li><p><strong>Excursions for a cruise ship</strong>: what is the smallest combination of outings and on-ship activities that will interest the largest number of passengers?</p>
</li>
<li><p><em>Similarly</em>: selecting political party platforms or legislative priorities; books for a mobile library; design templates for a app; destinations to fly to be a “complete” airline; and of course, the <em>original goal</em> of optimizing ad placement for reach; among many others.</p>
</li>
</ul>
<p><strong>Remember that TURF can use continuous estimates for “reach" (see note at the end)</strong> <strong>and thus can take advantage of any sort of estimates for interest or preference; and it can also weight respondents</strong>. The reach estimates may come from respondent choices, as in the data here, but also may come from observed data as in product instrumentation or a CRM system, or from a regression model. With so many options, there are many applications for TURF!</p>
<hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>As we saw, TURF analysis provides potential solutions to a very practical question: how to schedule a small number of events (weekly meeting times) for a group of people. Given a few candidate solutions, I was able to consider other factors such as practicality and create an initial schedule.</p>
<p>We discussed how TURF can be extended to larger and more complex cases, including ones where we have continuous (real number) estimates of interest instead of binary indications, and could weight respondents by some kind of importance estimate.</p>
<p><em>Could I have done this analysis by hand?</em> With this small data set and number of options, <em>yes</em>. OTOH, by doing it in code, I have an answer that is more scalable and will be easy to apply again as new data arrive. Plus I like coding (and also like blogging and explaining code).</p>
<p><strong>Learning More</strong>. Although TURF is widely used, I’m not aware of a great single source to learn more about it. The original paper was by Miaoulis et al (1990), listed in “Citations” below. To find more applications, I’d suggest to check <a target="_blank" href="https://scholar.google.com/scholar?cites=2630395812592527312">that paper’s citations</a> in Google Scholar and branch out from there. The <code>turfR</code> library also has helpful documentation and references.</p>
<p><strong>Try it!</strong> If you’d like to try TURF with another, more complex data set, <em>here’s a homework exercise</em>. Get the <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-1-data-averages">public MaxDiff data set with individual preferences</a> for classes to be offered by the Quant UX Association. What is the best set of 3, 4, or 5 classes that would reach the most people? <em>(Note: some choice modeling software offers TURF analysis built-in, but by adapting the code here, you can do it on your own in R!)</em></p>
<p>Best wishes!</p>
<hr />
<h2 id="heading-citations">Citations</h2>
<p>Horne J (2014). <strong>turfR</strong>: TURF Analysis for R. R package version 0.8-7, <a target="_blank" href="https://CRAN.R-project.org/package=turfR">https://CRAN.R-project.org/package=turfR</a>.</p>
<p>Miaoulis, G., Parsons, H., &amp; Free, V. (1990). TURF: A New Planning Approach for Product Line Extensions. <em>Marketing Research</em>, <em>2</em>(1).</p>
<p>Oksanen J, Simpson G, Blanchet F, Kindt R, Legendre P, Minchin P, O'Hara R, Solymos P, Stevens M, Szoecs E, Wagner H, Barbour M, Bedward M, Bolker B, Borcard D, Carvalho G, Chirico M, De Caceres M, Durand S, Evangelista H, FitzJohn R, Friendly M, Furneaux B, Hannigan G, Hill M, Lahti L, McGlinn D, Ouellette M, Ribeiro Cunha E, Smith T, Stier A, Ter Braak C, Weedon J, Borman T (2025). <strong>vegan</strong>: Community Ecology Package. R package version 2.6-10, <a target="_blank" href="https://CRAN.R-project.org/package=vegan">https://CRAN.R-project.org/package=vegan</a>.</p>
<p>Wickham H (2007). Reshaping Data with the <strong>reshape</strong> Package. Journal of Statistical Software, 21(12), 1-20. URL <a target="_blank" href="http://www.jstatsoft.org/v21/i12/">http://www.jstatsoft.org/v21/i12/</a>.</p>
<p>Wickham H (2016). <strong>ggplot2</strong>: Elegant Graphics for Data Analysis. Springer-Verlag New York.</p>
<hr />
<h2 id="heading-appendix-note-on-continuous-estimates-with-turf">Appendix: Note on Continuous Estimates with TURF</h2>
<p>TURF in general can work with continuous estimates if you define an appropriate conversion and/or estimation value for it to optimize. However, the most recent version of the <code>turfR</code> package doesn’t support that. It only uses 0 and 1 indicators. If you want to work with continuous data, there are at least three options:</p>
<ul>
<li><p><strong>Convert your data to 0/1 based on some sort of cutoff value.</strong> This is by far the most common option in practice, in my experience. Then use <code>turfR</code> exactly as shown above.</p>
<ul>
<li><p>For example, following is an R command that will take a data frame with continuous values and give a transformation such that the top 25% of values for each respondent are coded as “1” while all others are coded as “0”. (There are many, many variations and alternatives for such transformations. I’m sure there is a nice tidy function to do this, too, but I write base R by habit!)</p>
</li>
<li><p>This code uses <code>quantile()</code> to find a cutting point, and uses <code>apply()</code> to apply a binary cutting point function to each respondent (<code>margin=1</code>). Finally it uses <code>t()</code> to reshape that back to the expected row-x-column layout.</p>
</li>
</ul>
</li>
<li><pre><code class="lang-r">  data.bin &lt;- t(apply(data, margin=<span class="hljs-number">1</span>, 
                      <span class="hljs-keyword">function</span>(x) { as.numeric(x &gt;= quantile(x, probs=<span class="hljs-number">0.75</span>)) } ))
</code></pre>
</li>
</ul>
<ul>
<li><p><strong>Write your own TURF function or find one online</strong>. I’ve written TURF code at work — although unfortunately that lives with a former employer and I can’t share it — and will say it is not terribly complex. You could use the code in turfR as a starting point, or a <a target="_blank" href="https://bookdown.org/rossialessio095/R_Market_Research/turf.html">source like this one</a>. Just find the point where the comparison is made to 0/1 data or combinations and insert the continuous function you want instead (such as identifying the combo with the highest sum of values, etc.).</p>
</li>
<li><p><strong>Use a platform that implements TURF for the data format you have</strong>. For example, if you want preference estimates from MaxDiff or Conjoint Analysis, Sawtooth Software’s Lighthouse Studio and Discover products will do TURF with continuous estimates.</p>
</li>
</ul>
<hr />
<h2 id="heading-all-the-code">All the Code</h2>
<p>Following is all of the R code from this post, including inline code that creates the small data set.</p>
<pre><code class="lang-r"><span class="hljs-comment">####</span>
<span class="hljs-comment"># load our data</span>
<span class="hljs-comment"># note, data have been permuted from original responses,</span>
<span class="hljs-comment"># preserving overall characteristics while changed for each respondent (cf. vegan::permatswap)</span>
hourgrid &lt;- structure(c(<span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, 
  <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">1L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>, <span class="hljs-number">0L</span>), 
  dim = c(<span class="hljs-number">39L</span>, <span class="hljs-number">24L</span>), 
  dimnames = list(
    c(<span class="hljs-string">"1"</span>, <span class="hljs-string">"2"</span>, <span class="hljs-string">"3"</span>, <span class="hljs-string">"4"</span>, <span class="hljs-string">"5"</span>, <span class="hljs-string">"6"</span>, <span class="hljs-string">"7"</span>, <span class="hljs-string">"8"</span>, 
      <span class="hljs-string">"9"</span>, <span class="hljs-string">"10"</span>, <span class="hljs-string">"11"</span>, <span class="hljs-string">"12"</span>, <span class="hljs-string">"13"</span>, <span class="hljs-string">"14"</span>, <span class="hljs-string">"15"</span>, <span class="hljs-string">"16"</span>, <span class="hljs-string">"17"</span>, <span class="hljs-string">"18"</span>, <span class="hljs-string">"19"</span>, 
      <span class="hljs-string">"20"</span>, <span class="hljs-string">"21"</span>, <span class="hljs-string">"22"</span>, <span class="hljs-string">"23"</span>, <span class="hljs-string">"24"</span>, <span class="hljs-string">"25"</span>, <span class="hljs-string">"26"</span>, <span class="hljs-string">"27"</span>, <span class="hljs-string">"28"</span>, <span class="hljs-string">"29"</span>, <span class="hljs-string">"30"</span>, 
      <span class="hljs-string">"31"</span>, <span class="hljs-string">"32"</span>, <span class="hljs-string">"33"</span>, <span class="hljs-string">"34"</span>, <span class="hljs-string">"35"</span>, <span class="hljs-string">"36"</span>, <span class="hljs-string">"37"</span>, <span class="hljs-string">"38"</span>, <span class="hljs-string">"39"</span>), 
    c(<span class="hljs-string">"1:00"</span>, <span class="hljs-string">"2:00"</span>, <span class="hljs-string">"3:00"</span>, <span class="hljs-string">"4:00"</span>, <span class="hljs-string">"5:00"</span>, <span class="hljs-string">"6:00"</span>, <span class="hljs-string">"7:00"</span>, <span class="hljs-string">"8:00"</span>, <span class="hljs-string">"9:00"</span>, 
      <span class="hljs-string">"10:00"</span>, <span class="hljs-string">"11:00"</span>, <span class="hljs-string">"12:00"</span>, <span class="hljs-string">"13:00"</span>, <span class="hljs-string">"14:00"</span>, <span class="hljs-string">"15:00"</span>, <span class="hljs-string">"16:00"</span>, 
      <span class="hljs-string">"17:00"</span>, <span class="hljs-string">"18:00"</span>, <span class="hljs-string">"19:00"</span>, <span class="hljs-string">"20:00"</span>, <span class="hljs-string">"21:00"</span>, <span class="hljs-string">"22:00"</span>, <span class="hljs-string">"23:00"</span>, <span class="hljs-string">"0:00"</span>)
    )
  )
<span class="hljs-comment"># set that up as a nice dataframe and keep colnames as "hours"</span>
tmp.names &lt;- colnames(hourgrid)
hourgrid &lt;- data.frame(hourgrid)
names(hourgrid) &lt;- tmp.names
hourgrid$ID &lt;- <span class="hljs-number">1</span>:nrow(hourgrid) <span class="hljs-comment"># add ID variable needed later by TURF</span>
<span class="hljs-comment"># check its structure</span>
head(hourgrid)

<span class="hljs-comment"># basic stats</span>
<span class="hljs-comment"># count of preferred times</span>
colSums(hourgrid[ , <span class="hljs-number">1</span>:<span class="hljs-number">24</span>])
<span class="hljs-comment"># number of times chosen, per respondent</span>
rowSums(hourgrid[ , <span class="hljs-number">1</span>:<span class="hljs-number">24</span>])
<span class="hljs-comment"># how sparse is it?</span>
sum(hourgrid[ , <span class="hljs-number">1</span>:<span class="hljs-number">24</span>]) / nrow(hourgrid) / <span class="hljs-number">24</span>

<span class="hljs-comment"># heat map of times</span>
<span class="hljs-comment"># make heatmap</span>
<span class="hljs-keyword">library</span>(ggplot2)
<span class="hljs-keyword">library</span>(reshape2)
<span class="hljs-keyword">library</span>(car)
<span class="hljs-comment"># melt the data for nice ggplot structure</span>
hours.m &lt;- melt(subset(hourgrid, rowSums(hourgrid[ , <span class="hljs-number">1</span>:<span class="hljs-number">24</span>]) &gt; <span class="hljs-number">0</span>), 
                id.vars = <span class="hljs-string">"ID"</span>)
names(hours.m) &lt;- c(<span class="hljs-string">"Respondent"</span>, <span class="hljs-string">"Time"</span>, <span class="hljs-string">"Available"</span>)
<span class="hljs-comment"># some(hours.m, 15)</span>

<span class="hljs-comment"># plot it</span>
p &lt;- ggplot(data=hours.m, 
            aes(x=Respondent, y=Time, fill=Available)) +
  geom_tile(color = <span class="hljs-string">"grey90"</span>,
            lwd = <span class="hljs-number">0.5</span>,
            linetype = <span class="hljs-number">1</span>) +
  scale_fill_gradient(low = <span class="hljs-string">"white"</span>, high = <span class="hljs-string">"darkblue"</span>) +
  scale_y_discrete(limits = rev(levels(hours.m$Time))) +
  coord_fixed() +
  theme_minimal() +
  theme(panel.grid.major = element_blank(), panel.grid.minor = element_blank()) +
  theme(legend.position = <span class="hljs-string">"none"</span>) +
  xlab(<span class="hljs-string">"Respondent (permuted data)"</span>) +
  ylab(<span class="hljs-string">"Times Selected by Respondent (permuted data)"</span>)

p

<span class="hljs-comment"># TURF Analysis</span>
<span class="hljs-keyword">library</span>(turfR)

<span class="hljs-comment"># set up the data for TURF package</span>
<span class="hljs-comment"># add ID variable so respondents are identified to TURF</span>
turf.dat &lt;- data.frame(
  ID = hourgrid[ , <span class="hljs-string">"ID"</span>],           <span class="hljs-comment"># respondent ids</span>
  Weight = rep(<span class="hljs-number">1</span>, nrow(hourgrid)),  <span class="hljs-comment"># the weight / importance for each respondent</span>
  hourgrid[, <span class="hljs-number">1</span>:(ncol(hourgrid)-<span class="hljs-number">1</span>)]  <span class="hljs-comment"># columns of data to use; all except "ID"</span>
)
head(turf.dat)

<span class="hljs-comment"># run the TURF analysis for best 2 times</span>
(turf.2 &lt;- turf(turf.dat, n=<span class="hljs-number">24</span>, k=<span class="hljs-number">2</span>, keep=<span class="hljs-number">10</span>))$turf

<span class="hljs-comment"># est'd CI for the TURF #1 solution</span>
<span class="hljs-comment"># get the proportion reached for solution #1</span>
p.turf &lt;- turf.3$turf[[<span class="hljs-number">1</span>]]$rchX[<span class="hljs-number">1</span>]
<span class="hljs-comment"># estimate of 80% confidence interval (swag; bootstrap etc would be better)</span>
<span class="hljs-comment"># set the Z value (standard errors) as appropriate; in this case "80% CI" Z == 1.28</span>
ci     &lt;- <span class="hljs-number">1.28</span> * sqrt(p.turf * (<span class="hljs-number">1</span>-p.turf) / nrow(hourgrid))
c(p.turf - ci, p.turf + ci)

<span class="hljs-comment"># what if we scheduled 3 times instead of 2? </span>
(turf.3 &lt;- turf(turf.dat, n=<span class="hljs-number">24</span>, k=<span class="hljs-number">3</span>, keep=<span class="hljs-number">15</span>))$turf
</code></pre>
<p><a target="_blank" href="https://notbyai.fyi"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746999945541/9d224843-9e9f-44cc-98c5-276915794420.png" alt class="image--center mx-auto" /></a></p>
]]></content:encoded></item><item><title><![CDATA[Announcing: Tech Community Zen]]></title><description><![CDATA[Today I’m sharing something different: the launch of a new Zen meditation practice group for the online tech community. We’ll start practice May 27, 2025, with an initial time of 9am Pacific (12p Eastern, 18p CEST).
If you’re only here for Quant UX d...]]></description><link>https://quantuxblog.com/announcing-tech-community-zen</link><guid isPermaLink="true">https://quantuxblog.com/announcing-tech-community-zen</guid><category><![CDATA[zen]]></category><category><![CDATA[Tech community]]></category><category><![CDATA[meditation]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Tue, 15 Apr 2025 18:46:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1743090081478/cff5dafd-7818-4e41-b70a-3bf3a9f69b43.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Today I’m sharing something different: the launch of a <strong>new Zen meditation practice group for the online tech community</strong>. We’ll start practice <em>May 27, 2025</em>, with an initial time of 9am Pacific (12p Eastern, 18p CEST).</p>
<p>If you’re only here for Quant UX discussion, you can skip the rest of this newsletter. However, I’ve heard from some readers interested in Zen. If you are one, keep reading. Or visit <a target="_blank" href="https://tczen.org">Tech Community Zen</a>!</p>
<hr />
<h2 id="heading-brief-background">Brief Background</h2>
<p>Apart from my work and writing, I’m an <a target="_blank" href="https://www.tczen.org/about-us">ordained Zen teacher</a> (aka “priest”). I started Zen practice in 2001, was ordained as a Dharma Teacher in 2015, and received additional teaching authorization (“Inka”) in 2023.</p>
<p><strong>I’m starting Tech Community Zen for a simple reason: the tech community can benefit from Zen</strong>. I see the tech community experiencing chaos, with many people hoping for something stable and fulfilling. Zen practice can be part of an answer to that chaos. Its benefits extend to our colleagues, families, communities, and the world around us.</p>
<p>Zen teaches deep attention, helping others, compassion, and counteracting destructive desire and anger. In these times, the tech community — and the whole world — needs those.</p>
<hr />
<h2 id="heading-what-is-tech-community-zen">What is Tech Community Zen?</h2>
<p><strong>Tech Community Zen practices authentic Zen online as a community</strong> … and will develop from there. This includes <em>group practice</em> (Zazen; meditation) and, for those interested, <em>individual koan practice</em> (if that doesn’t mean anything to you, don’t worry; I’m mentioning it for anyone who wonders).</p>
<p>Depending on interest over time, we might add other online or in-person events such as intensive retreats. We’ll see!</p>
<p>Why does this focus on the tech community? Attention to the tech community is beneficial for two reasons: the community needs it, and the focus helps with teaching, programming, expectations, and community interaction. At the same time, <strong>everyone is welcomed</strong> and no one is excluded.</p>
<hr />
<h2 id="heading-interested-help-with-the-planning">Interested? Help with the Planning!</h2>
<p>You can find out more — and help plan the schedule and activities — in two ways:</p>
<ol>
<li><p><strong>Share your preferences</strong>, especially for days &amp; times for online practice, in this short survey:<br /> <a target="_blank" href="https://surveys.sawtoothsoftware.com/67c8902733613c9ad1b9818e">Tech Community Zen Interest Survey</a></p>
</li>
<li><p><strong>Sign up</strong> for the Tech Community Zen newsletter:<br /> <a target="_blank" href="https://tech-community-zen-newsletter.beehiiv.com/subscribe">https://tech-community-zen-newsletter.beehiiv.com/subscribe</a></p>
</li>
</ol>
<h3 id="heading-more-questions"><em>More Questions?</em></h3>
<p>Visit the FAQ and other information pages at <a target="_blank" href="https://tczen.org">tczen.org</a> (== <a target="_blank" href="https://techcommunityzen.org">techcommunityzen.org</a>). Or email and ask: <a target="_blank" href="mailto:techcommunityzen@gmail.com">techcommunityzen@gmail.com</a></p>
<p>Cheers, and I’ll return to Quant UX specific topics in the next post :)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748018386060/1113c068-803c-413b-aa15-b75956044def.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Four areas of (UXR) thinking about AI / LLMs]]></title><description><![CDATA[This post is my attempt to make sense (largely for myself) of various trends and responses about AI / LLMs, and thematic disagreements that I observe among UX Researchers discussing LLMs.
I structure the conversations I’ve recently had with UXRs abou...]]></description><link>https://quantuxblog.com/four-areas-of-uxr-thinking-about-ai-llms</link><guid isPermaLink="true">https://quantuxblog.com/four-areas-of-uxr-thinking-about-ai-llms</guid><category><![CDATA[quantux]]></category><category><![CDATA[uxresearch]]></category><category><![CDATA[AI]]></category><category><![CDATA[Ethics in AI]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Tue, 18 Mar 2025 16:34:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/v0Lu245JUOI/upload/7fa3a1f1d5dc41aa4c3ff348408f6f8a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This post is my attempt to make sense (largely for myself) of various trends and responses about AI / LLMs, and thematic disagreements that I observe among UX Researchers discussing LLMs.</p>
<p>I structure the conversations I’ve recently had with UXRs about LLMs into four thematic areas, giving a few pro &amp; con arguments in each. <strong>I’m writing this not to advance specific claims but instead to share a schema that has been useful to me</strong>.</p>
<p>Thanks to corporate interests, there is a lot of “pro AI” discussion, and the claims on that side are well known. On the other hand, social pressures act to obscure “con” arguments, and I spend slightly more time on them. Although I outline both pro &amp; con, <strong>I’m not advocating for either “both sides” or “no one knows.”</strong> Instead, my hope here is that any reader (and especially me) will gain clarity from the considerations and can thereby have a stronger, more specific, and more coherent framework wherever they land.</p>
<p>I outline the following four general areas:</p>
<ul>
<li><p><strong>Utility</strong></p>
</li>
<li><p><strong>Aesthetics</strong></p>
</li>
<li><p><strong>Social Context &amp; Reaction</strong></p>
</li>
<li><p><strong>Ethics &amp; Externalities</strong></p>
</li>
</ul>
<p>I present this in terms of “what UXRs are thinking”. That is only because my conversations are most often with UXRs. <strong>By understanding these areas, I hope we can disentangle arguments about LLMs</strong>, such as when claims of <em>utility</em> are pitted against <em>aesthetic</em> or <em>ethical</em> perceptions. After laying out the framework, I recount a small illustration of how it may help us think about claims that people make.</p>
<hr />
<h2 id="heading-theme-1-utility">Theme 1: Utility</h2>
<p>The first pattern is the most obvious for UX researchers: <strong>consideration of the <em>utility</em> of AI</strong> in delivering use cases for a product, or similarly, in helping with UXR activities.</p>
<p>On the pro side there are <a target="_blank" href="https://github.com/NiuTrans/ABigSurveyOfLLMs?tab=readme-ov-file#section29"><em>utility claims</em></a> that LLMs deliver new experiences, assists in running surveys, can analyze user responses, summarize documents, meeting, or transcripts, draft reports, write code, and so forth.</p>
<p>On the negative side, I see two typical patterns:</p>
<ul>
<li><p>Claims that LLMs <em>don’t</em> make things better, or <a target="_blank" href="https://www.lesswrong.com/posts/tqmQTezvXGFmfSe7f/how-much-are-llms-actually-boosting-real-world-programmer">not much better</a>, or are not worth the effort (cf. my <a target="_blank" href="https://quantuxblog.com/thoughts-on-llm-ai-to-learn-r-programming">article on R coding</a>)</p>
</li>
<li><p>Claims that LLMs trade off short term gains for long-term harms in utility (cf. some studies in <a target="_blank" href="https://arxiv.org/html/2410.01026v1#S6">this review of AI for developer productivity</a>, or in this study for <a target="_blank" href="https://www.microsoft.com/en-us/research/uploads/prod/2025/01/lee_2025_ai_critical_thinking_survey.pdf">general cognition</a>).</p>
</li>
</ul>
<p>A common view is that any AI application has a “game theory” payoff matrix:</p>
<ul>
<li><p>Potential <em>benefits</em> when AI is correct or helpful</p>
</li>
<li><p>vs. Potential <em>risks</em> when AI is incorrect or misleading</p>
</li>
</ul>
<p>Some activities have low risk (e.g., recommender systems, role playing chat, games) whereas others have high risk (e.g., finance, health case, security). A complete game theoretic model could additionally include the risks and benefits from ethical considerations and externalities as noted below.</p>
<p>A <a target="_blank" href="https://aial.ie/pages/aiparis/">criticism of that sort of game theoretic view</a> is that it is based on (1) an assumption of a “net profit” model where an LLM delivers some positive value, and the question is how problems trade off against that value; and (2) that assumes that such trade-offs can be made at all. As I’m arguing here, <strong>some of the trade-offs are between incommensurate areas</strong>, such that a game theory approach — although not “wrong” as such — is limited at best and strongly misleading at worst.</p>
<p>Overall, utility is clearly the #1 area of discussion for UXRs and product teams generally.</p>
<blockquote>
<p>Side note: unlike classic utilitarian philosophy, <strong>“utility” in the AI context is often used reductively to mean “[individual] productivity.”</strong> To the extent that such claims align with classic, longer-run utilitarian considerations, they often fall into the <a target="_blank" href="https://en.wikipedia.org/wiki/TESCREAL">problems</a> of so-called “effective altruism” and related movements. That caveat is worth noting although depth discussion is out of scope here.</p>
</blockquote>
<hr />
<h2 id="heading-theme-2-aesthetics">Theme 2: Aesthetics</h2>
<p>The second area I see is aesthetic evaluation of AI (again, for purposes here, meaning LLMs). By this, I mean the evaluation of the <strong><em>beauty, tastefulness, appeal</em></strong>, and similar properties of AI output. <em>This is not restricted to AI-generated art</em> but applies to any AI output, such as the “beauty” of AI-written code, reports, texts, etc.</p>
<p>Although many people are using AI-generated texts and graphics, I’m not aware of positive claims that the aesthetics of those outputs are actually <em>good</em> (<a target="_blank" href="https://www.sciencedirect.com/science/article/pii/S0747563223000584">related study</a>). Instead, AI output appears to be regarded by its users as aesthetically tolerable. They often comment that AI results are <strong>easy or “democratized”</strong>.</p>
<p>On a closely related positive note, there are many UXRs and others who are <strong>fascinated</strong> by AI interactions and who enjoy engaging with AI systems at length. I would not categorize that enjoyment as <em>aesthetic</em> but rather as more generally <em>emotionally engaging</em>.</p>
<p>On the negative side, I observe three primary areas of aesthetic complaint:</p>
<ul>
<li><p><strong>Claims that AI products are aesthetically bad</strong>, such as being ugly, badly written, “<a target="_blank" href="https://www.techradar.com/computing/artificial-intelligence/ai-slop-is-taking-over-the-internet-and-ive-had-enough-of-it">slop</a>,” and the like. This view seems to be common among <a target="_blank" href="https://whatever.scalzi.com/2023/02/23/omg-is-the-ai-coming-for-my-job/">professional writers</a>, <a target="_blank" href="https://www.cityweekly.net/utah/the-case-for-not-messing-with-ai-art/Content?oid=21297415">artists</a>, and musicians.</p>
<ul>
<li><p>Many developers see AI code as needing <a target="_blank" href="https://www.infoworld.com/article/3610521/refactoring-ai-code-the-good-the-bad-and-the-weird.html">large scale refactoring</a> before it is “good.”</p>
</li>
<li><p>Differently, the Authors’ Guild — verified published authors, of which I’m a member — recently launched a “human authored” <a target="_blank" href="https://authorsguild.org/news/ag-launches-human-authored-certification-to-preserve-authenticity-in-literature/">certification program</a> to guarantee “authentic” works.</p>
</li>
</ul>
</li>
<li><p>Worries that <strong>AI proliferation will drown out good work</strong>, rewrite aesthetic preferences, or generally lead people to miss the important aspects of creation while focusing on mere output and “productivity”. A <a target="_blank" href="https://www.hachettebookgroup.com/titles/john-warner/more-than-words/9781541605510/?lens=basic-books">new book, More than Words, by John Warner</a> discusses the latter angle in depth.</p>
</li>
<li><p>A more generalized feeling that AI <strong>diminishes the appreciation of human creativity</strong> and human experiences, and that it devalues the many years and thousands of hours of effort that it takes to master a craft such as writing, drawing, painting, music, or programming.</p>
</li>
</ul>
<p>Overall, some professional writers, developers, artists, and musicians experience AI output with a sense of revulsion. Although such an aesthetic response is emotional, I would note that it is <em>not</em> <em>only</em> emotional.</p>
<p>In my view, <strong>there are good reasons to treat aesthetic truth as being on par with other kinds of truths</strong> such as propositional (classically logical) truth. However, that discussion goes deep into philosophy and related areas such as cultural studies, and that is out of scope here (for more, a starting philosopher, which is not to say an <em>easy</em> philosopher, is <a target="_blank" href="https://plato.stanford.edu/entries/adorno/">Adorno</a>).</p>
<p><strong>How does this relate to the category of utility above?</strong> Answer: aesthetics and utility are separate. An interaction between product team members might go like this:</p>
<ul>
<li><p><em>Person A:</em> This LLM letter-writing feature will lower the bar substantially for new writers [a utility claim]</p>
</li>
<li><p><em>Person B</em>: But its output is awful to read, so bland and tasteless [an aesthetic claim]</p>
</li>
</ul>
<p>By sorting evaluations into these two categories, as well as the subcategories noted, we can better evaluate competing claims (and identify when they are not competing but are just <em>different</em>).</p>
<hr />
<h2 id="heading-theme-3-social-context-amp-reaction">Theme 3: Social Context &amp; Reaction</h2>
<p>A third set of responses to AI depend on the interplay of any particular UXR’s own social situation and personality.</p>
<p>As we all know, AI has had widespread pressure for institutional adoption, and has also suffered from tremendous hype. At times this has been paired with claims — such as those about AGI — that are somewhere between fraudulent and incredibly ignorant (I’ve written about <a target="_blank" href="https://quantuxblog.com/were-far-from-agi">AGI</a> and <a target="_blank" href="https://quantuxblog.com/debunking-llm-iq-test-results">AI “IQ”</a> as examples).</p>
<p><strong>Each UXR responds to the social context of AI according to their own needs and personal style</strong>. While one UXR may wish to go along with demands, another may rebel against authority or hype. One may emphasize the future promise of AI, while another will see LLMs as a tool of corporate subjugation.</p>
<p>The impacts of AI in social contexts are not only evolving but also are more complex than any single person could observe. As in the old story about blind men and an elephant, different aspects appear with different people.</p>
<p>On the negative side, I’ve observed particularly hostile reactions prompted by the following:</p>
<ul>
<li><p><strong>Hype in general</strong>. Some people detest hype and react against it (<a target="_blank" href="https://www.dair-institute.org/maiht3k/">the Mystery AI Hype podcast</a> is an example)</p>
</li>
<li><p><strong>Illogic</strong>. Some UXRs have a particular dislike of illogical claims made about AI, as they also might in other domains (<em>nb</em>: I personally have this tendency!)</p>
</li>
<li><p><strong>Management demands</strong>. Some UXRs expect the worse from management (perhaps because of <a target="_blank" href="https://quantuxblog.com/the-end-of-tech-as-a-big-family">past trauma</a>) and may view AI as an extension of managerial or corporate domination.</p>
</li>
</ul>
<p>In the specific UXR context, the pro &amp; con trends for social context show up in claims such as:</p>
<ul>
<li><p><em>[pro]</em> "AI is the future and UXers have to learn it”</p>
</li>
<li><p><em>[con]</em> “Management doesn’t understand what they’re talking about, and we need to stop the harm to users”</p>
</li>
<li><p><em>[pro &amp; con]</em> “Excessive hype is destroying opportunities to talk about the real value of AI”</p>
</li>
<li><p><em>[pro]</em> “AI is an area where I can show thought leadership” or “By using AI, I can get ahead”</p>
</li>
</ul>
<p>Again — as we saw above for claims of utility vs. aesthetics — <strong>the set of issues regarding the social positioning of AI is logically separate from the other areas outlined here</strong>. If someone says, “We need to learn AI to get ahead,” that doesn’t imply anything in particular, positively or negatively, about whether AI is useful, aesthetic, or as I discuss next, ethical.</p>
<hr />
<h2 id="heading-theme-4-ethics-amp-externalities">Theme 4: Ethics &amp; Externalities</h2>
<h3 id="heading-preliminary-points"><em>Preliminary Points</em></h3>
<p>This is the longest section, because <strong>ethical issues are perhaps the most contentious claims about AI</strong>. Extreme positive arguments claim that AI is a force for good that will solve cancer, mitigate climate change, and eliminate human drudgery. On the extreme negative side, AI is said to be a short term tool of theft, oppression, corporatism, and militarism, and a long term existential risk to human and the world ecosystem.</p>
<p>Before outlining my framework, I have two overall observations. First, <strong>my impression is that ethical takes are often derived post hoc</strong>. They may be used to reinforce stances that one has <em>already</em> adopted for reasons of aesthetics, emotions, self interest, etc. In other words, I believe UXRs often either like AI or not, for various reasons, and then adopt “ethical” reasons to justify a stance retroactively. I’m not saying that is always the case, but often occurs (and that is perfectly OK, in my view — there is no reason why ethics should be one’s <em>first</em> consideration, as long as they are an <em>eventual</em> consideration).</p>
<p>Second, and closely related, I observe that <strong>ethical takes are generally unpersuasive to anyone who doesn’t already hold them</strong>. The proponents of ethical arguments appear to hope that they will be persuasive to others — whether that is “AI will cure cancer!” or that “AI is theft!” — but I have seen no good evidence that such arguments are persuasive to anyone (in the sense of changing opinions).</p>
<p>Now, <strong>my claim that ethical arguments are unpersuasive does not imply that ethical arguments are <em>wrong</em></strong>, nor that they are useless. Ethical positions may be useful for providing a backstop and support for views held otherwise, for giving a “record” for posterity, and for deepening one’s own thinking and moral actions. Persuasion is not the only salient outcome. (<em>Full disclosure</em>: I think about ethics a lot because I was a philosophy graduate student prior to psychology, and I wrote a philosophy Master’s thesis in ethics. I also taught the mandatory Research Ethics class to over 1500 employees when I worked at Google.)</p>
<p>With those preliminaries in place, the following points outline how I see ethical issues lining up. (<em>n.b.</em>, this area is vast and I don’t pretend that these points are complete).</p>
<h3 id="heading-ethics-in-ai-training"><em>Ethics in AI Training</em></h3>
<ul>
<li><p>Positive claims:</p>
<ul>
<li><p>LLMs are a vastly <a target="_blank" href="https://copyrightblog.kluweriplaw.com/2024/02/29/is-generative-ai-fair-use-of-copyright-works-nyt-v-openai/">scaled-up version</a> of <strong>fair use</strong>, making massive information available to everyone</p>
</li>
<li><p>Training creates jobs in economically disadvantaged areas like Africa and Southeast Asia</p>
</li>
</ul>
</li>
<li><p>Negative claims:</p>
<ul>
<li><p>LLMs are only possible because they use <strong>stolen</strong> <strong>intellectual property</strong> (<a target="_blank" href="https://www.debevoise.com/insights/publications/2025/02/an-early-win-for-copyright-owners-in-ai-cases-as">example</a>)</p>
</li>
<li><p>The <a target="_blank" href="https://www.theguardian.com/technology/2023/aug/02/ai-chatbot-training-human-toll-content-moderator-meta-openai">workers who train LLMs</a> are <strong>underpaid</strong> and treated abusively, undergoing substantial <strong>trauma</strong></p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-ethics-in-ai-applications-and-other-short-term-results"><em>Ethics in AI Applications and Other Short-Term Results</em></h3>
<ul>
<li><p>Positive claims:</p>
<ul>
<li><p>LLMs deliver various <strong>features and productivity enhancement</strong> (refer to the “utility” section above)</p>
</li>
<li><p>LLMs <strong>help people such as non-native speakers or people with disabilities</strong> achieve parity with others (<a target="_blank" href="https://www.technologyreview.com/2024/08/23/1096607/ai-people-with-disabilities-accessibility/">example</a>)</p>
</li>
</ul>
</li>
<li><p>Negative claims:</p>
<ul>
<li><p>LLM features have generally <strong>low marginal value</strong> (<a target="_blank" href="https://www.techradar.com/phones/new-survey-suggests-the-vast-majority-of-iphone-and-samsung-galaxy-users-find-ai-useless-and-to-be-honest-im-not-surprised">example</a>), and may worsen products they go into</p>
</li>
<li><p>LLM features may exist not to deliver value but to <strong>capture personal data</strong> for corporate purposes</p>
</li>
<li><p>LLMs contribute to <strong>declining skills</strong> in reading, writing, and <a target="_blank" href="https://www.microsoft.com/en-us/research/wp-content/uploads/2025/01/lee_2025_ai_critical_thinking_survey.pdf">critical thinking</a></p>
</li>
<li><p>The push for LLMs brings an <strong>opportunity cost</strong> of less product attention to <a target="_blank" href="https://uxdesign.cc/no-ai-user-research-is-not-better-than-nothing-its-much-worse-5add678ab9e7">real user needs</a></p>
</li>
<li><p>LLMs push all outputs towards a style of <strong>white, western, English language, educated, affluent, and heteronormative modes</strong> because that style dominates their training data</p>
</li>
<li><p>LLM data centers capture <strong>water and electricity</strong> needed by homes, businesses, and agriculture</p>
</li>
<li><p>Contrary to claims that they achieve parity, <strong>LLMs perpetuate</strong> <a target="_blank" href="https://www.psu.edu/news/information-sciences-and-technology/story/trained-ai-models-exhibit-learned-disability-bias-ist"><strong>stereotypes and biases</strong></a> about disabled people and other groups</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-externalities-and-structural-issues-current-as-opposed-to-hypothetical"><em>Externalities and Structural Issues (current, as opposed to hypothetical)</em></h3>
<ul>
<li><p>Positive claims:</p>
<ul>
<li><p>AI productivity (see “<em>utility</em>” above) will help companies achieve <strong>higher profits</strong></p>
</li>
<li><p>AI is helpful for <strong>non-native speakers</strong> (by which, proponents often mean “<em>of English</em>”) and helps them write better, grammatically, etc.</p>
</li>
</ul>
</li>
<li><p>Negative claims:</p>
<ul>
<li><p>LLMs contribute to the increasing <strong>domination and centering of white, male, affluent English</strong> as a structural norm (similar to the point in “applications” above). (Examples: <a target="_blank" href="https://dl.acm.org/doi/abs/10.1145/3630106.3658975">1</a>, <a target="_blank" href="https://ojs.aaai.org/index.php/AIES/article/view/31758">2</a>, <a target="_blank" href="https://www.mcpdigitalhealth.org/article/S2949-7612\(24\)00020-8/fulltext">3</a>, <a target="_blank" href="https://drive.google.com/file/d/16F_JZv4eHNiDMJT6BT7F6m97C2rBX8-7/view?usp=sharing">4</a>.)</p>
</li>
<li><p>Promises (or realities) of AI productivity are contributing to <strong>job losses</strong></p>
</li>
<li><p>LLMs devalue workers and <strong>replace creators</strong>, leading to a race to the bottom for artistic creation</p>
</li>
<li><p>LLMs increase the <strong>concentration of power</strong> among a few companies and their billionaires, achieving higher profit and more power with fewer workers and worker protections</p>
</li>
<li><p>LLM data centers <strong>pollute</strong> the planet and contribute to acceleration of <a target="_blank" href="https://sloanreview.mit.edu/article/tackling-ais-climate-change-problem/">climate change</a></p>
</li>
<li><p>LLMs <a target="_blank" href="https://medium.com/@jl115/harm-from-language-models-discrimination-exclusion-and-toxicity-b94d7cf44c57">differentially hurt</a> <strong>marginal communities</strong> (non-white, low-wage, global south, etc.)</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-ethics-amp-hypothetical-future-results"><em>Ethics &amp; Hypothetical Future Results</em></h3>
<ul>
<li><p>Positive claims (speculative):</p>
<ul>
<li><p>AI speeds up scientific discovery, and new discoveries could <strong>mitigate many scientific problems</strong> (medical research, climate change, etc.)</p>
</li>
<li><p>AI could usher in an <strong>era of productivity</strong> such that people are freed to work less, more creatively</p>
</li>
</ul>
</li>
<li><p>Negative claims(speculative):</p>
<ul>
<li><p>Bugs or malicious usage of AI pose a <strong>range of threats</strong>, ranging from <em>modest scale</em> problems (fraud, impersonation, theft) to <em>socially catastrophic</em> (financial collapse) to <em>existential</em> (engineered viruses, killer machines, nuclear war)</p>
</li>
<li><p>AI may lead to humans being massively unemployed as <strong>jobs are replaced by AI</strong>, and largely confined to “lower skilled” labor that AI cannot replace</p>
</li>
<li><p>LLMs <a target="_blank" href="https://link.springer.com/article/10.1007/s11948-025-00529-0">change <strong>expectations about truth</strong></a> and the veracity of both information and work products</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-research-specific-ethical-issues"><em>Research-Specific Ethical Issues</em></h3>
<ul>
<li><p>Positive claims:</p>
<ul>
<li><p>LLMs <strong>democratize and decrease the cost of research</strong> by providing synthetic training data and by analyzing data faster (<a target="_blank" href="https://medium.com/towards-data-science/how-llms-will-democratize-exploratory-data-analysis-70e526e1cf1c">example claim</a>)</p>
</li>
<li><p>LLMs can discover new things, faster, by <strong>exploring research domains in new ways</strong></p>
</li>
</ul>
</li>
<li><p>Negative claims:</p>
<ul>
<li><p>Research applications depend on the <strong>uncredited work</strong> of other researchers</p>
</li>
<li><p><strong>“Democratizing research” as machine output devalues research</strong>. Normalizing <em>research-as-production</em> is to abandon the value of researchers and their contributions (and jobs).</p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/pulse/dangers-risks-depending-synthetic-data-from-large-language-jdihf/">Synthetic data is <strong>misleading and biased</strong></a>, over-representing dominant social groups (as above) and specifically downplaying and <strong>systematically under-estimating marginalized groups</strong></p>
</li>
<li><p>Subjecting respondent and user data to LLM systems <strong>violates privacy and informed consent (</strong><a target="_blank" href="https://www.sciencedirect.com/science/article/pii/S2667295225000042"><strong>1</strong></a><strong>,</strong> <a target="_blank" href="https://fpf.org/blog/lets-look-at-llms-understanding-data-flows-and-risks-in-the-workplace/"><strong>2</strong></a><strong>)</strong></p>
</li>
<li><p>LLM research results <strong>amplify what is obvious</strong> in a data set and/or is “centroid” in its training data, rather than discovering new things or representing actual data (<a target="_blank" href="https://arxiv.org/html/2503.01631v1">related</a>)</p>
</li>
<li><p>Using a system with ethical problems (if one believes that) is itself a <strong>research ethics violation</strong></p>
</li>
</ul>
</li>
</ul>
<p>In some discussions, folks go farther about their concerns with LLMs. <strong>In those cases, some UXRs and managers actively question the ethics</strong> of pressure that they feel to research or use AI.</p>
<p>That ethical questioning takes two forms. One form is <strong>whether it is ethical to do UX research about AI</strong> that is required to say something they don’t believe — namely, that LLMs add value. I often hear researchers complaining that they are being “forced” to find reasons that AI will add product value. In those cases, they feel that the users and customers they represent are being ignored or even actively harmed. They question whether there is any point in doing research that is forced to “validate” preordained executive decisions.</p>
<blockquote>
<p>Side note: research usually is not directly ordered to support a decision (although that sometimes happens). Rather, results are demanded through organizational pressure, such as: having negative results challenged; being asked to do A/B testing between options that both assume some need (AI, for example); being promoted or praised for work that extolls the desired outcome; etc. Such mechanisms bias results but preserve management ability to believe that research is “honestly sharing data”.</p>
</blockquote>
<p>The second form of UXR ethical questioning is at a higher level: <strong>whether it is ethical to use LLMs at all</strong> or to work for a company that promotes them or requires one to use them. That arises if one holds some of the negative ethical views above. Similarly — but as far as I can tell, much less frequently — a UXR who is an AI proponent may have ethical concerns about NOT using AI tools. This may occur when they are in an organization that is not using LLMs because of concerns about privacy, data protection, and the like.</p>
<hr />
<h2 id="heading-how-the-themes-help-a-brief-example">How the Themes Help: a Brief Example</h2>
<p>Here’s <strong>a small example of how I personally find this framework to be helpful</strong>. Yesterday I was reading a LinkedIn post and comment that went like this (I’m not going to call out anyone, so I paraphrase):</p>
<ul>
<li><p><strong>Post</strong>: AI tools seem so potentially useful for research! What are people doing with them?</p>
</li>
<li><p><strong>Comment</strong>: I use them all the time. Anyone not using them is a dinosaur and will be left behind.</p>
</li>
</ul>
<p>In the past, I would have read this sort of exchange as annoyingly <em>lacking in information</em>, as being <em>hype</em>, and as <em>name-calling</em> (“dinosaur”) that uses belittlement instead of informed discourse (perhaps to cover up the commenter’s own anxiety).</p>
<p>However, using the 4 themes outlined here, I now decode it as follows:</p>
<ul>
<li><p><strong>Utility</strong>: there is a implicit claim of utility but no evidence</p>
</li>
<li><p><strong>Aesthetics</strong>: <em>ignored</em></p>
</li>
<li><p><strong>Social</strong> <strong>context</strong>: an explicit claim that AI can help one get ahead, again without evidence</p>
</li>
<li><p><strong>Ethics</strong>: <em>ignored</em></p>
</li>
</ul>
<p>In short, the exchange boils down to an explicit claim that AI can help someone advance, and it ignores (or somewhat denigrates) the other potential positions. I imagine this reflects anxiety more than anything else.</p>
<p>With the 4 elements of the framework, the argument can be deconstructed to see (a) where it is making claims and not making claims; and — just as importantly — (b) partially defused of emotion, anger, confusion, and the like, because it is clearer what is going on.</p>
<hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>If you made it this far, congratulations :) As I noted at the beginning, my goal here has been to organize some areas of discourse and debate for my own thinking.</p>
<p>Reviewing that framework, a few themes and observations stand out:</p>
<ul>
<li><p><strong>The four themes here — utility, aesthetics, social function, and ethics — are mostly incommensurate</strong> and not directly comparable. That leads to two things:</p>
<ul>
<li><p>The <em>various sides in debates tend to miss each others’ points</em> by responding across domains, such as answering a utility claim with an ethical rebuttal or vice versa. That convinces no one.</p>
</li>
<li><p><em>People simply ignore themes</em> where the prevailing argument don’t agree with them. For example, proponents of utility dismiss may concerns about aesthetics and ethics.</p>
</li>
</ul>
</li>
<li><p>After outlining the themes here, I observe that most of <strong>the discussion about AI reflects a strongly productivist orientation</strong> — in society as well as in product teams and among UXRs. LLMs are discussed in terms of how much they help people produce, how fast one can write reports or code or create images, how much data one analyze, how many articles summarized, and so forth.</p>
</li>
<li><p>The <strong>UXRs I talk with are increasingly frustrated</strong> at the situation because it leads to untenable demands on research. I believe much of that results from two things:</p>
<ul>
<li><p>The incommensurability of the themes as outlined. <strong>Researchers have increasing cognitive dissonance and dissatisfaction as the thematic areas collide</strong>. For example, they may have ethical concerns competing with pressure to be productive; or pressure to support an AI feature competing with user data that suggests dissatisfaction. That leads to sharply decreased researcher morale.</p>
</li>
<li><p>They feel <strong>particular tension around the productivist orientation</strong>, that research is to be judged primarily on the basis of faster and more output (as opposed, say, to user benefit, or truth).</p>
</li>
</ul>
</li>
<li><p><strong>None of this implies a both-sides equivalence</strong>. I outline the pro and con arguments not to equate them but instead to reflect that different people hold different views. Any given UXR will — and IMO <em>should</em> — take a principled stance that aligns with their own perspective. To reiterate, my goal in this article was to clarify the issues, so I can better understand the positions I take.</p>
</li>
</ul>
<p>Finally, I would note that none of these tensions are new with AI. The tensions between research demands, ethics, productivity, and so forth have always existed in product research. However, the confusions and tensions appear to be worsening rapidly. I think that reflects two things:</p>
<ol>
<li><p>With its expectations, hype, and extremes, <strong>AI brings the preexisting tensions into sharper and more intense focus.</strong></p>
</li>
<li><p><strong>This is happening at a time of</strong> <a target="_blank" href="https://quantuxblog.com/the-end-of-tech-as-a-big-family"><strong>organizational and “cultural” turmoil within tech</strong></a>, where UXRs already feel acutely anxious. With that baseline anxiety and uncertainty, the intensified tensions of AI are more difficult to understand, process, or resolve..</p>
</li>
</ol>
<p>What should we do about it? I don’t have specific recommendations for now. Instead, what I believe is that <strong>awareness of the situation is the first step towards finding whatever is next</strong>.</p>
<p>I hope these reflections and an outline of the themes will contribute to such awareness. Cheers!</p>
<p><a target="_blank" href="https://notbyai.fyi"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746999945541/9d224843-9e9f-44cc-98c5-276915794420.png" alt class="image--center mx-auto" /></a></p>
]]></content:encoded></item><item><title><![CDATA[Tinkering with Job Statistics]]></title><description><![CDATA[Introduction
I recently read comments from politicians that “college should prepare students for high paying jobs.” Without getting into politics, it left me wondering: what were the high-paying jobs when I went to college?
In this post, I detail how...]]></description><link>https://quantuxblog.com/tinkering-with-job-statistics</link><guid isPermaLink="true">https://quantuxblog.com/tinkering-with-job-statistics</guid><category><![CDATA[quantux]]></category><category><![CDATA[uxresearch]]></category><category><![CDATA[R Language]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Thu, 06 Mar 2025 15:24:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741215643819/1f0f6198-b8d7-4242-bcd6-4e855a4b469f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>I recently read comments from politicians that “college should prepare students for high paying jobs.” Without getting into politics, it left me wondering: <em>what were the high-paying jobs when</em> <strong><em>I</em></strong> <em>went to college</em>?</p>
<p>In this post, I detail how I found government statistics from 1985 — the year I went to college at age 17 — and <strong>used R to extract data from a PDF file</strong> and make a simple “opportunity plot.”</p>
<p>I’m not claiming this is a complete (nor even a good) answer to the question about what jobs someone should choose. Instead, it’s a fun way to get data from a difficult source and then look back at jobs data from 40 years ago — and develop some R skills!</p>
<p>As always, I share R code along the way, and I compile it again at the end.</p>
<hr />
<h2 id="heading-getting-amp-cleaning-the-jobs-data">Getting &amp; Cleaning the Jobs Data</h2>
<p>The data I used come from the US Bureau of Labor Statistics in a <a target="_blank" href="https://www.bls.gov/opub/mlr/1985/10/rpt1full.pdf">PDF here</a> (as of this writing), published in October 1985 (Prieser, 1985). This PDF file appears to be converted from a scanned document with optical character recognition (OCR).</p>
<p>The tables of interest show <strong>job counts and salary</strong>, on Page 2. Here is an excerpt, which among other things shows a slight slant to the page reflecting its presumed origin as a scanned document:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741224272870/3c9e35f9-1213-4a30-aebb-ad147a1dd1b4.png" alt class="image--center mx-auto" /></p>
<p>What I will do in R is this:</p>
<ol>
<li><p>Read the job names, counts, and salaries from the PDF</p>
</li>
<li><p>Clean up the data</p>
</li>
<li><p>Group the rows into higher level categories (“Accountants”, “Chemists”, etc.)</p>
</li>
<li><p>Plot a 2×2 showing job counts vs salaries</p>
</li>
</ol>
<p>To get the data from the PDF, I’ll use the <code>tabulapdf</code> package in R (Sepulveda, 2024).</p>
<blockquote>
<p>Installation note: <code>tabulapdf</code> uses a Java library, and thus requires a local installation of Java Runtime Engine (JRE) and the Java Development Kit (JDK). That installation process is somewhat complex, so I will skip it here; but there are notes in the Appendix below on how I did it on my Macbook.</p>
</blockquote>
<p>First, I download the <a target="_blank" href="https://www.bls.gov/opub/mlr/1985/10/rpt1full.pdf">PDF document</a> from the BLS site and save it locally to <code>~/Downloads</code>. Using <code>tabulapdf</code>, I extract “Table 2” from that file as <strong>two separate tables</strong> (one for each column). If you download the PDF and update the <code>filename</code> variable for wherever you put it and name it, then you should be able to follow along.</p>
<p>Here is the code:</p>
<pre><code class="lang-r">filename &lt;- <span class="hljs-string">"~/Downloads/bls-report-1985.pdf"</span>  <span class="hljs-comment"># update for your folder and filename</span>
<span class="hljs-keyword">library</span>(tabulapdf)                             <span class="hljs-comment"># see Appendix for installation notes</span>
tables &lt;- extract_areas(filename, pages=c(<span class="hljs-number">2</span>, <span class="hljs-number">2</span>))
tables[[<span class="hljs-number">1</span>]]
</code></pre>
<p>In <code>tabulapdf</code>, there are options either to extract tables automatically (<code>extract_tables()</code>) or to tell it exactly where you are interested by <strong>selecting rectangular areas</strong> with <code>extract_areas()</code>. Our PDF has several tables with complex formatting, so I used <code>extract_areas()</code> to tell it exactly where to look.</p>
<p>In <code>extract_areas()</code>, the option <code>pages=c(2, 2)</code> tells it to look on page 2 and then to let me select 2 areas for extraction. When I run that, page 2 appears in the RStudio viewer. I <strong>click and drag to select each of the two areas</strong> in turn as shown here:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741225167662/27238096-1489-46ac-beed-f4b605494c4d.png" alt class="image--center mx-auto" /></p>
<p>The initially-converted first column (<code>tables[[1]]</code>) looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741224863032/1fb05773-4cac-4023-8f3a-f2b3b5255048.png" alt class="image--center mx-auto" /></p>
<p><strong>Success</strong>! However, there is a lot of junk in the data such as ellipses, extra spaces, a “$” symbol, commas, etc. Next, we’ll clean that up.</p>
<blockquote>
<p><em>To extract data from a</em> <em>different PDF</em>, <em>simply change the</em> <code>filename</code> <em>as above and then alter the</em> <code>pages=</code> <em>argument as needed for your file. Or see the help pages for</em> <code>tabulapdf</code> <em>for more options.</em></p>
</blockquote>
<hr />
<h2 id="heading-cleaning-the-data">Cleaning the Data</h2>
<p><code>tabulapdf</code> gave us 2 tables, one for each “column” in the PDF. To proceed, we’ll first <strong>combine those into one data set</strong> and then <strong>clean up messy data</strong> that comes from 1980s table formatting and OCR scanning.</p>
<p>To combine the two column sets, I select only the columns with data we want — namely, columns 1, 11, and 12 from the left hand part of the table and 1, 7, and 8 from the right hand part. Then I set friendly names and bind them into a single data frame:</p>
<pre><code class="lang-r">df1 &lt;- data.frame(tables[[<span class="hljs-number">1</span>]])[ , c(<span class="hljs-number">1</span>, <span class="hljs-number">11</span>, <span class="hljs-number">12</span>)]
df2 &lt;- data.frame(tables[[<span class="hljs-number">2</span>]])[ , c(<span class="hljs-number">1</span>,  <span class="hljs-number">7</span>,  <span class="hljs-number">8</span>)]
names(df1) &lt;- names(df2) &lt;- c(<span class="hljs-string">"Job"</span>, <span class="hljs-string">"N"</span>, <span class="hljs-string">"Salary"</span>)
job.data &lt;- na.omit(rbind(df1, df2))   <span class="hljs-comment"># removes header rows from the tables</span>
</code></pre>
<p>In this case, <code>na.omit()</code> removes rows without complete data (such as within-table header lines).</p>
<p>Next, I remove junk from the data and convert the numeric columns to numbers:</p>
<pre><code class="lang-r"><span class="hljs-comment"># clean up ellipses in the job names</span>
job.data[ , <span class="hljs-number">1</span>]       &lt;- gsub(<span class="hljs-string">" \\."</span>, <span class="hljs-string">""</span>, job.data[ , <span class="hljs-number">1</span>])  
<span class="hljs-comment"># remove nuisance spaces, etc., and convert numeric columns to numbers</span>
job.data[ , c(<span class="hljs-number">2</span>, <span class="hljs-number">3</span>)] &lt;- lapply(job.data[ , c(<span class="hljs-number">2</span>, <span class="hljs-number">3</span>)],      
                                 <span class="hljs-keyword">function</span>(x) as.numeric(gsub(<span class="hljs-string">"[^0-9.-]"</span>, <span class="hljs-string">""</span>, x)))
</code></pre>
<p>In this code, the first line uses <code>gsub()</code> to <strong>remove all instances of “</strong> <code>.</code><strong>”</strong> (the ellipses in job titles). The second line uses <code>gsub()</code> to <strong>remove all non-numeric characters</strong> from the 2 number columns, and then converts them to numeric data type using <code>as.numeric()</code>. (If you’re wondering, one could use tidyverse functions and/or base R pipes. I default to “very” base R, but everything in R has multiple solutions!)</p>
<p>So far, our data look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741226033851/7ade9278-a441-4a71-baf9-b5a6c98b5d47.png" alt class="image--center mx-auto" /></p>
<p>Next we’ll <strong>group</strong> all of the Accountants together, Chemists together, and so forth. For the first step, we split each job title where there is a spaces + parenthesis (“ <code>(</code>“) to separate the parenthetically noted GS scales, and keep only the text that appears before that point, such as “Accountants I”. To do that, I apply <code>str_split()</code> from the <code>stringr</code> package to each job title, using an anonymous function that retains only the first part (<code>[1]</code>) of each split string:</p>
<pre><code class="lang-r"><span class="hljs-keyword">library</span>(stringr)
<span class="hljs-comment"># Split on "(" [job levels] and only keep the descriptive part before that</span>
job.data$JobGroup &lt;- sapply(job.data$Job, 
                              <span class="hljs-keyword">function</span>(x) str_split(x, <span class="hljs-string">" \\("</span>, simplify = <span class="hljs-literal">TRUE</span>)[<span class="hljs-number">1</span>])
</code></pre>
<p>Next I <strong>remove the Roman numerals</strong> (“I”, “IV” etc.) where they appear, and apply <code>trimws()</code> to strip off any left over white space (thanks to Ben Bolker for the regex in <a target="_blank" href="https://stackoverflow.com/questions/64595514/r-remove-roman-numerals-from-column">this post</a>).</p>
<pre><code class="lang-r">job.data$JobGroup &lt;- trimws(gsub(<span class="hljs-string">'([IVXLCM]+)\\.?$'</span>,<span class="hljs-string">''</span>, job.data$JobGroup))
</code></pre>
<p>While inspecting the data, I see 3 rows where OCR problems made problems for the <code>JobGroup</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741226604873/1ea608e7-57ee-450c-97cf-8bf24b44ba52.png" alt class="image--center mx-auto" /></p>
<p>So I <strong>fix those manually</strong>:</p>
<pre><code class="lang-r">job.data[c(<span class="hljs-number">96</span>, <span class="hljs-number">106</span>, <span class="hljs-number">107</span>), <span class="hljs-string">"JobGroup"</span>] &lt;- c(<span class="hljs-string">"Purchasing assistants"</span>, <span class="hljs-string">"Typists"</span>, <span class="hljs-string">"Typists"</span>)
</code></pre>
<p>Finally, I convert <code>JobGroup</code> to a factor (nominal) variable and review the result:</p>
<pre><code class="lang-r"><span class="hljs-comment"># Make the result into a factor variable</span>
job.data$JobGroup &lt;- factor(job.data$JobGroup)
<span class="hljs-comment"># check the data</span>
summary(job.data)
head(job.data, <span class="hljs-number">8</span>)
</code></pre>
<p><strong>It’s working</strong> and we see that all of the Accountants are grouped (and Chief accountants are a different group — which is OK for now), and so forth:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741226750297/8707723c-f09c-46c8-8668-678cbffc35b3.png" alt class="image--center mx-auto" /></p>
<p>Next we’ll compute the total employment and average salary per <code>JobGroup</code>.</p>
<blockquote>
<p><em>For an important project, I’d do a deeper review to make sure I caught all of the data issues. For instance, I see some potential OCR errors like “GS-2” for "Accountants II”. For purposes of this post, I’ll simply go ahead and use the data as extracted to this point.</em></p>
</blockquote>
<hr />
<h2 id="heading-grouping-the-data">Grouping the Data</h2>
<p>Now that we have assigned job groups, we need to <strong>aggregate them for total employment</strong> (sum of the N column per group) <strong>and average salary</strong> (taking the weighted average of Salary x N).</p>
<p>The tricky part is <strong>how to apply the</strong> <code>weighted.mean()</code> <strong>function in R across multiple groups</strong>. As always, there are multiple options (tidyverse, <code>by()</code>, a custom function, etc.) but a <a target="_blank" href="https://stackoverflow.com/questions/33692439/using-aggregate-to-compute-monthly-weighted-average">simple base R solution</a> is to use an index column inside <code>aggregate()</code>. Here’s the code:</p>
<pre><code class="lang-r"><span class="hljs-comment"># tip from https://stackoverflow.com/questions/33692439/using-aggregate-to-compute-monthly-weighted-average </span>
job.data$row  &lt;- <span class="hljs-number">1</span>:nrow(job.data)
<span class="hljs-comment"># get the weighted mean salaries per group</span>
job.sum       &lt;- aggregate(row ~ JobGroup, job.data, 
                           <span class="hljs-keyword">function</span>(i) weighted.mean(job.data$Salary[i], job.data$N[i])) 
<span class="hljs-comment"># rename the "row" column</span>
names(job.sum)[<span class="hljs-number">2</span>] &lt;- <span class="hljs-string">"AvgSalary"</span>
</code></pre>
<p>In this code, the current <code>row</code>(s) that correspond to each <code>JobGroup</code> are passed by <code>aggregate()</code> to the anonymous function, which then returns a <code>weighted.mean()</code> for the variables of interest in those rows.</p>
<p>The next step is to sum up the number of jobs in each JobGroup, which is a simple <code>aggregate(…, sum)</code>:</p>
<pre><code class="lang-r"><span class="hljs-comment"># find the total number of jobs per group</span>
job.sum$Total &lt;- aggregate(N ~ JobGroup, job.data, sum)$N
</code></pre>
<p>That’s it! Now we have <strong>data aggregated by job group with counts and weighted average salaries</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741230488782/ada9e699-63d0-470e-854d-8fec05969bf5.png" alt class="image--center mx-auto" /></p>
<p>Next I’ll plot those.</p>
<hr />
<h2 id="heading-making-an-opportunity-plot">Making an Opportunity Plot</h2>
<p>A simple thought is that the “best jobs” are those that have the highest combination of availability and salary. (I’ll leave aside the question of <em>future-looking</em> availability and salary!) A <strong>2×2 interpretation of a scatter plot</strong> is one way to examine that.</p>
<p>I plot <strong>average salary vs. job count</strong> in the BLS data as follows:</p>
<pre><code class="lang-r"><span class="hljs-keyword">library</span>(ggplot2)
<span class="hljs-keyword">library</span>(ggrepel)
<span class="hljs-keyword">library</span>(scales)
p &lt;- ggplot(data=job.sum, 
            aes(x=Total, y=AvgSalary, label=JobGroup)) +
  geom_point(color=<span class="hljs-string">"red"</span>) +
  geom_text_repel() +
  scale_x_log10(labels = label_number()) +
  xlab(<span class="hljs-string">"Total Employment (log scale)"</span>) +
  ylab(<span class="hljs-string">"Average Salary, weighted across levels"</span>) +
  theme_minimal() +
  ggtitle(<span class="hljs-string">"Salary vs. Employment, US Statistics for 1985"</span>)

p
</code></pre>
<p>Here are a couple of notes on that code. First, it uses <code>ggrepel</code> (Slowikowski, 2024) to place the <strong>text labels</strong> (job groups) in readable positions. Second, it puts the X axis on a <strong>log scale</strong> for compactness; otherwise the engineers group would distort it substantially. Third, it uses <code>scales</code> (Wickham et al, 2023) to label the X axis more legibly. Finally, <code>theme_minimal()</code> removes some chart junk.</p>
<p>Here’s the result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741230905633/dc3f8a14-3208-450c-912b-bb7363f59777.png" alt class="image--center mx-auto" /></p>
<p>We see <strong>two particularly interesting groups</strong>. In the upper right hand side, there is a group of relatively to very common professions that are also relatively highly paid in 1985: Engineers, Systems analysts, and Chemists. In the upper left, there is a group of less common professions that are even more highly paid: Chief accountants, Directors of personnel, and Attorneys. (Those are likely all true today, as well!)</p>
<p>What would we do for <strong>further analysis</strong>? First, I’d want to include <strong>additional data</strong> that (presumably) is in other BLS data sets. For example, <em>health care and education professionals</em>, among others, are not included here. Second, we might use their data (page 1 in the PDF) on <strong>trends</strong> and changes to project forward.</p>
<p>Third, within the data set, we could consider some sort of implied <strong>transition among career levels</strong> to look at more of a <em>career-spanning</em> set of expectations. For example, the movement from one level to a higher one, within a job group, might be modeled as a <strong>Markov chain</strong> (somehow combining data across years, and/or within a data set, and/or using the change metrics — problems one would want to consider). Fourth, we could <strong>adjust salary levels</strong> to make them comparable across years, using inflation-adjusted values or the like.</p>
<hr />
<h2 id="heading-back-to-1985">Back to 1985</h2>
<p><strong>What about my choices in 1985?</strong> My personal college major was not influenced at all by expectation of salary or availability! I completed a double major in <em>Psychology and Comparative Religion</em>, and went (initially) to graduate school in <em>Philosophy</em>. That all reflected personal interest and a planned academic career. Eventually I changed to graduate school in <em>Clinical Psychology</em>. Data such as these would have been of no use or concern to 17 year old me (except as a matter of curiosity — which I still have).</p>
<p>What I can say, 40 years later, is this: I ended up in an uncommon profession (Quant UX Research) that is also highly paid — and was nowhere to be found in the world or data of 1985! My completely <em>non-job-related</em> education was perfect preparation in my case. I’ll write more about that another time.</p>
<p>I would, at a first approximation, do exactly the same thing again. But the data are still interesting!</p>
<hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>This post demonstrated <strong>how to get data from a PDF — even a messy, slanted, older, erratically-OCR’d, scanned PDF — into R</strong>. And that let us have some fun (at least in my opinion) to look at jobs from way back when I started college.</p>
<p>Cheers and I hope you find it useful!</p>
<hr />
<h3 id="heading-appendix-notes-on-installing-tabulapdf-and-its-java-dependencies"><em>Appendix: Notes on installing</em> <code>tabulapdf</code> <em>and its Java dependencies</em></h3>
<p>As I noted above, <code>tabulapdf</code> requires Java — both the runtime engine (aka “Java”, aka “JRE”) and the developer kit (JDK) — and the integration of those with R using <code>rJava</code> (Urbanek 2024). Installing those may be tricky depending on the details of your system.</p>
<p>On my M2 Macbook, here’s what I did. The <code>rJavaEnv</code> package (Kotov, 2024) was a particularly nice &amp; helpful touch to install the JDK and get everything set up inside R. The steps:</p>
<pre><code class="lang-r"><span class="hljs-comment">### Appendix: setting up rJava on Mac OS X machine</span>

<span class="hljs-comment"># This is the sequence I used on MacBook with M2, may vary with other systems</span>

<span class="hljs-comment"># A1. Install Java runtime (if needed; usually is)</span>
<span class="hljs-comment"># ... https://www.java.com/en/download/</span>
<span class="hljs-comment"># ... installed relevant JRE (Java 8 Update 441, for Mac 64-bit ARM)</span>

<span class="hljs-comment"># A2. install the rJava library in R</span>
install.packages(<span class="hljs-string">"rJava"</span>)
<span class="hljs-comment"># check that rJava is working:</span>
<span class="hljs-keyword">library</span>(<span class="hljs-string">"rJava"</span>)

<span class="hljs-comment"># A3. install Java development kit using rJavaEnv in R</span>
<span class="hljs-comment">#    (rJavaEnv helps get everything working correctly)</span>
install.packages(<span class="hljs-string">"rJavaEnv"</span>)
<span class="hljs-keyword">library</span>(rJavaEnv)
java_quick_install()        <span class="hljs-comment"># gets and installs the JDK, then sets system pointers</span>
java_check_version_rjava()  <span class="hljs-comment"># should show "21" + "21.0.6" or some similar/later version</span>

<span class="hljs-comment"># A4. install Tabula PDF library and dependencies needed for interactive usage in R</span>
install.packages(<span class="hljs-string">"tabulapdf"</span>)
install.packages(c(<span class="hljs-string">"shiny"</span>, <span class="hljs-string">"miniUI"</span>))

<span class="hljs-comment"># A5. quit R / RStudio, reboot Mac to make sure everything is squared away and reloaded</span>

<span class="hljs-comment"># A6. test installation using tabulapdf data set</span>
<span class="hljs-keyword">library</span>(rJava)
<span class="hljs-keyword">library</span>(tabulapdf)
<span class="hljs-comment"># the following code is updated from tabulapdf vignette</span>
f   &lt;- system.file(<span class="hljs-string">"examples"</span>, <span class="hljs-string">"mtcars.pdf"</span>, package = <span class="hljs-string">"tabulapdf"</span>)
out &lt;- extract_tables(f)
str(out)
out[[<span class="hljs-number">1</span>]]   <span class="hljs-comment"># the "mtcars" data</span>
</code></pre>
<p>If you run into issues, my main recommendations are (1) to update everything, (2) to make sure all installers (Java, R, and RStudio) are using matching CPU builds (e.g., 64-bit ARM), (3) reinstall, reboot, and (4) search for answers online.</p>
<p>I will note that there is not a lot of help online for debugging Java integration with R. It’s a bit of an edge case, although a highly useful one as we’ve seen.</p>
<hr />
<h2 id="heading-all-the-code">All the Code</h2>
<p>As always, I share all of my R code in one place for simplicity. Here it is, including the Appendix:</p>
<pre><code class="lang-r"><span class="hljs-comment"># Using R to parse 1985 jobs stats in a PDF</span>
<span class="hljs-comment"># Chris Chapman, March 2025</span>
<span class="hljs-comment"># for quantuxblog.com</span>

<span class="hljs-comment"># <span class="hljs-doctag">NOTE:</span> the tabulapdf package requires Java; see appendix for setup notes</span>

<span class="hljs-comment"># 1. load BLS data for 1985</span>
<span class="hljs-comment"># get job data from US Bureau of Labor Statistics</span>
<span class="hljs-comment"># note: was working as of March 5, 2025</span>
filename &lt;- <span class="hljs-string">"~/Downloads/bls-report-1985.pdf"</span>   <span class="hljs-comment"># update for your folder and filename</span>
<span class="hljs-keyword">library</span>(tabulapdf)                              <span class="hljs-comment"># see Appendix for installation notes</span>
tables &lt;- extract_areas(filename, pages=c(<span class="hljs-number">2</span>, <span class="hljs-number">2</span>))
tables[[<span class="hljs-number">1</span>]]

<span class="hljs-comment"># 2. convert those to usable R format as a single data set</span>
<span class="hljs-comment">#    get the 3 main columns; bind them to 1 data frame</span>
df1 &lt;- data.frame(tables[[<span class="hljs-number">1</span>]])[ , c(<span class="hljs-number">1</span>, <span class="hljs-number">11</span>, <span class="hljs-number">12</span>)]
df2 &lt;- data.frame(tables[[<span class="hljs-number">2</span>]])[ , c(<span class="hljs-number">1</span>,  <span class="hljs-number">7</span>,  <span class="hljs-number">8</span>)]
names(df1) &lt;- names(df2) &lt;- c(<span class="hljs-string">"Job"</span>, <span class="hljs-string">"N"</span>, <span class="hljs-string">"Salary"</span>)
job.data &lt;- na.omit(rbind(df1, df2))   <span class="hljs-comment"># removes header rows from the tables</span>

<span class="hljs-comment"># 2.1 clean up the data to remove spaces, etc.</span>
<span class="hljs-comment"># clean up ellipses in the job names</span>
job.data[ , <span class="hljs-number">1</span>]       &lt;- gsub(<span class="hljs-string">" \\."</span>, <span class="hljs-string">""</span>, job.data[ , <span class="hljs-number">1</span>])  
<span class="hljs-comment"># remove nuisance spaces, etc., and convert numeric columns to numbers</span>
job.data[ , c(<span class="hljs-number">2</span>, <span class="hljs-number">3</span>)] &lt;- lapply(job.data[ , c(<span class="hljs-number">2</span>, <span class="hljs-number">3</span>)],      
                                 <span class="hljs-keyword">function</span>(x) as.numeric(gsub(<span class="hljs-string">"[^0-9.-]"</span>, <span class="hljs-string">""</span>, x)))
job.data

<span class="hljs-comment"># 2.2 create job group factor that collapses the levels</span>
<span class="hljs-keyword">library</span>(stringr)
<span class="hljs-comment"># Split on "(" [job levels] and only keep the descriptive part before that</span>
job.data$JobGroup &lt;- sapply(job.data$Job, 
                              <span class="hljs-keyword">function</span>(x) str_split(x, <span class="hljs-string">" \\("</span>, simplify = <span class="hljs-literal">TRUE</span>)[<span class="hljs-number">1</span>])
<span class="hljs-comment"># Remove the Roman numerals from the descriptions (another part of job levels)</span>
job.data$JobGroup &lt;- trimws(gsub(<span class="hljs-string">'([IVXLCM]+)\\.?$'</span>,<span class="hljs-string">''</span>, job.data$JobGroup))

<span class="hljs-comment"># And finally, fix 3 extraction errors manually</span>
job.data[c(<span class="hljs-number">96</span>, <span class="hljs-number">106</span>, <span class="hljs-number">107</span>), ]
job.data[c(<span class="hljs-number">96</span>, <span class="hljs-number">106</span>, <span class="hljs-number">107</span>), <span class="hljs-string">"JobGroup"</span>] &lt;- c(<span class="hljs-string">"Purchasing assistants"</span>, <span class="hljs-string">"Typists"</span>, <span class="hljs-string">"Typists"</span>)
<span class="hljs-comment"># Make the result into a factor variable</span>
job.data$JobGroup &lt;- factor(job.data$JobGroup)
<span class="hljs-comment"># check the data</span>
summary(job.data)
head(job.data, <span class="hljs-number">8</span>)

<span class="hljs-comment"># 3. get the total employment and weighted salaries per group</span>
<span class="hljs-comment"># create a row counter we can use to index the data when aggregating by group</span>
<span class="hljs-comment"># thanks for this tip, https://stackoverflow.com/questions/33692439/using-aggregate-to-compute-monthly-weighted-average </span>
job.data$row  &lt;- <span class="hljs-number">1</span>:nrow(job.data)
<span class="hljs-comment"># get the weighted mean salaries per group</span>
job.sum       &lt;- aggregate(row ~ JobGroup, job.data, 
                           <span class="hljs-keyword">function</span>(i) weighted.mean(job.data$Salary[i], job.data$N[i])) 
<span class="hljs-comment"># rename the "row" column</span>
names(job.sum)[<span class="hljs-number">2</span>] &lt;- <span class="hljs-string">"AvgSalary"</span>
<span class="hljs-comment"># find the total number of jobs per group</span>
job.sum$Total &lt;- aggregate(N   ~ JobGroup, job.data, sum)$N

head(job.sum)


<span class="hljs-comment"># 4. plot "opportunity" as # jobs vs average salary</span>
<span class="hljs-keyword">library</span>(ggplot2)
<span class="hljs-keyword">library</span>(ggrepel)
<span class="hljs-keyword">library</span>(scales)
p &lt;- ggplot(data=job.sum, 
            aes(x=Total, y=AvgSalary, label=JobGroup)) +
  geom_point(color=<span class="hljs-string">"red"</span>) +
  geom_text_repel() +
  scale_x_log10(labels = label_number()) +
  xlab(<span class="hljs-string">"Total Employment (log scale)"</span>) +
  ylab(<span class="hljs-string">"Average Salary, weighted across levels"</span>) +
  theme_minimal() +
  ggtitle(<span class="hljs-string">"Salary vs. Employment, US Statistics for 1985"</span>)

p

<span class="hljs-comment">### Appendix: setting up rJava on Mac OS X machine</span>

<span class="hljs-comment"># This is the sequence I used on MacBook with M2, may vary with other systems</span>

<span class="hljs-comment"># A1. Install Java runtime (if needed; usually is)</span>
<span class="hljs-comment"># ... https://www.java.com/en/download/</span>
<span class="hljs-comment"># ... installed relevant JRE (Java 8 Update 441, for Mac 64-bit ARM)</span>

<span class="hljs-comment"># A2. install the rJava library in R</span>
install.packages(<span class="hljs-string">"rJava"</span>)
<span class="hljs-comment"># check that rJava is working:</span>
<span class="hljs-keyword">library</span>(<span class="hljs-string">"rJava"</span>)

<span class="hljs-comment"># A3. install Java development kit using rJavaEnv in R</span>
<span class="hljs-comment">#    (rJavaEnv helps get everything working correctly)</span>
install.packages(<span class="hljs-string">"rJavaEnv"</span>)
<span class="hljs-keyword">library</span>(rJavaEnv)
java_quick_install()        <span class="hljs-comment"># gets and installs the JDK, then sets system pointers</span>
java_check_version_rjava()  <span class="hljs-comment"># should show "21" + "21.0.6" or some similar/later version</span>

<span class="hljs-comment"># A4. install Tabula PDF library and dependencies needed for interactive usage in R</span>
install.packages(<span class="hljs-string">"tabulapdf"</span>)
install.packages(c(<span class="hljs-string">"shiny"</span>, <span class="hljs-string">"miniUI"</span>))

<span class="hljs-comment"># A5. quit R / RStudio, reboot Mac to make sure everything is squared away and reloaded</span>

<span class="hljs-comment"># A6. test installation using tabulapdf data set</span>
<span class="hljs-keyword">library</span>(rJava)
<span class="hljs-keyword">library</span>(tabulapdf)
<span class="hljs-comment"># the following code is updated from tabulapdf vignette</span>
f   &lt;- system.file(<span class="hljs-string">"examples"</span>, <span class="hljs-string">"mtcars.pdf"</span>, package = <span class="hljs-string">"tabulapdf"</span>)
out &lt;- extract_tables(f)
str(out)
out[[<span class="hljs-number">1</span>]]   <span class="hljs-comment"># the "mtcars" data</span>
</code></pre>
<hr />
<h2 id="heading-references">References</h2>
<p>Chang W, Cheng J, Allaire J, Sievert C, Schloerke B, Xie Y, Allen J, McPherson J, Dipert A, Borges B (2024). <em>shiny: Web Application Framework for R</em>. R package version 1.10.0, <a target="_blank" href="https://CRAN.R-project.org/package=shiny">https://CRAN.R-project.org/package=shiny</a>.</p>
<p>Cheng J (2018). <em>miniUI: Shiny UI Widgets for Small Screens</em>. R package version 0.1.1.1, <a target="_blank" href="https://CRAN.R-project.org/package=miniUI">https://CRAN.R-project.org/package=miniUI</a>.</p>
<p>Kotov E (2024). <em>rJavaEnv: Java Environments for R Projects</em>. doi:10.32614/CRAN.package.rJavaEnv <a target="_blank" href="https://doi.org/10.32614/CRAN.package.rJavaEnv">https://doi.org/10.32614/CRAN.package.rJavaEnv</a>, <a target="_blank" href="https://github.com/e-kotov/rJavaEnv">https://github.com/e-kotov/rJavaEnv</a>.</p>
<p>Prieser C (1985). “Occupational salary levels for white-collar workers, 1985”. <em>Monthly Labor Review,</em> Bureau of Labor Statistics, October 1985. At <a target="_blank" href="https://www.bls.gov/opub/mlr/1985/10/rpt1full.pdf">https://www.bls.gov/opub/mlr/1985/10/rpt1full.pdf</a>, retrieved March 5, 2025.</p>
<p>R Core Team (2025). <em>R: A Language and Environment for Statistical Computing</em>. R Foundation for Statistical Computing, Vienna, Austria. <a target="_blank" href="https://www.R-project.org/">https://www.R-project.org/</a>.</p>
<p>Sepulveda MV (2024). <em>tabulapdf: Extract Tables from PDF Documents</em>. <a target="_blank" href="https://github.com/ropensci/tabulapdf">https://github.com/ropensci/tabulapdf</a>.</p>
<p>Slowikowski K (2024). <em>ggrepel: Automatically Position Non-Overlapping Text Labels with 'ggplot2'</em>. R package version 0.9.5, <a target="_blank" href="https://CRAN.R-project.org/package=ggrepel">https://CRAN.R-project.org/package=ggrepel</a>.</p>
<p>Urbanek S (2024). <em>rJava: Low-Level R to Java Interface</em>. R package version 1.0-11, <a target="_blank" href="https://CRAN.R-project.org/package=rJava">https://CRAN.R-project.org/package=rJava</a>.</p>
<p>Wickham H (2016). <em>ggplot2: Elegant Graphics for Data Analysis</em>. Springer-Verlag New York.</p>
<p>Wickham H, Pedersen T, Seidel D (2023). <em>scales: Scale Functions for Visualization</em>. R package version 1.3.0, <a target="_blank" href="https://CRAN.R-project.org/package=scales">https://CRAN.R-project.org/package=scales</a>.</p>
<p>Wickham H (2023). <em>stringr: Simple, Consistent Wrappers for Common String Operations</em>. R package version 1.5.1, <a target="_blank" href="https://CRAN.R-project.org/package=stringr">https://CRAN.R-project.org/package=stringr</a>.</p>
<p><a target="_blank" href="https://notbyai.fyi"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746999945541/9d224843-9e9f-44cc-98c5-276915794420.png" alt class="image--center mx-auto" /></a></p>
]]></content:encoded></item><item><title><![CDATA[Individual Scores in Choice Models, Part 4: Inspecting Model Fit with RLH]]></title><description><![CDATA[I expect this to be the last post of my “Individual Scores in Choice Models” series. To recap the series so far:

Post 1 discussed the data — real data on UXRs’ preferences among Quant classes on a MaxDiff survey — and it reviewed the stack rank of p...]]></description><link>https://quantuxblog.com/individual-scores-in-choice-models-part-4-inspecting-model-fit-with-rlh</link><guid isPermaLink="true">https://quantuxblog.com/individual-scores-in-choice-models-part-4-inspecting-model-fit-with-rlh</guid><category><![CDATA[quantux]]></category><category><![CDATA[conjoint]]></category><category><![CDATA[maxdiff]]></category><category><![CDATA[R Language]]></category><category><![CDATA[survey research]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Tue, 14 Jan 2025 15:22:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/wF0dXfsxY8g/upload/904ef1285fa61efc5d45dad29bf32845.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I expect this to be the last post of my “Individual Scores in Choice Models” series. To recap the series so far:</p>
<ul>
<li><p><a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-1-data-averages">Post 1 discussed the data</a> — <em>real</em> data on UXRs’ preferences among Quant classes on a MaxDiff survey — and it reviewed the stack rank of preferences and the individual <em>distribution</em> of preferences.</p>
</li>
<li><p><a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-2-correlations-among-items">Post 2 examined patterns of correlation</a> among UXRs’ class preferences.</p>
</li>
<li><p><a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-3-respondent-segments">Post 3 discussed one process to look for useful Segments</a> among respondents.</p>
</li>
</ul>
<p><strong>In this 4th post, I examine the Root Likelihood (RLH) fit measure</strong>, which summarizes how well MaxDiff (or Conjoint) utilities fit the observed data from survey respondents. It’s a long post, partly because the concept needs explanation, but also because I want to share closely-related ideas along the way.</p>
<p>This is only a partial discussion of quality assessment overall, because <strong>there are <em>many</em> ways to review data quality and model fit</strong>. You should add RLH inspection to other methods you might already use, such as identifying speeders. (However, I have warnings below about filtering respondents!)</p>
<p>As always, I share R code along the way. You can follow along live with the actual data — thanks to the generous sharing of anonymized data by the <a target="_blank" href="https://quantuxcon.org">Quant UX Association</a>!</p>
<hr />
<h2 id="heading-get-the-data-to-follow-along">Get the Data to Follow Along</h2>
<p>The data here are <strong>individual estimates of interest in Quant UX training classes for N=308</strong> mostly UXR respondents, as assessed on a MaxDiff survey. You can find the details in <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-1-data-averages">Post 1</a>.</p>
<p>The following code loads the data needed for this post, and renames the variables to be shorter &amp; friendly:</p>
<pre><code class="lang-r"><span class="hljs-comment"># get the data; repeating here for blog post 4, see post 1 for details</span>
<span class="hljs-keyword">library</span>(openxlsx)   <span class="hljs-comment"># install if needed, as with all package calls</span>
md.dat &lt;- read.xlsx(<span class="hljs-string">"https://quantuxbook.com/misc/QUX%20Survey%202024%20-%20Future%20Classes%20-%20MaxDiff%20Individual%20raw%20scores.xlsx"</span>)   <span class="hljs-comment"># </span>
md.dat$Anchor &lt;- <span class="hljs-literal">NULL</span>
names(md.dat)[<span class="hljs-number">3</span>:<span class="hljs-number">16</span>] &lt;- c(<span class="hljs-string">"Choice Models"</span>,  <span class="hljs-string">"Surveys"</span>,       <span class="hljs-string">"Log Sequences"</span>,    <span class="hljs-string">"Psychometrics"</span>, 
                         <span class="hljs-string">"R Programming"</span>,  <span class="hljs-string">"Pricing"</span>,       <span class="hljs-string">"UX Metrics"</span>,       <span class="hljs-string">"Bayes Stats"</span>,
                         <span class="hljs-string">"Text Analytics"</span>, <span class="hljs-string">"Causal Models"</span>, <span class="hljs-string">"Interviewer-ing"</span>,  <span class="hljs-string">"Advanced Choice"</span>, 
                         <span class="hljs-string">"Segmentation"</span>,   <span class="hljs-string">"Metrics Sprints"</span>)
classCols &lt;- <span class="hljs-number">3</span>:ncol(md.dat)    <span class="hljs-comment"># generally, Sawtooth exported utilities start in column 3</span>
</code></pre>
<hr />
<h2 id="heading-model-fit-background-what-is-rlh">Model Fit Background: What is RLH?</h2>
<p><em>[This section is long because I explain RLH conceptually, using simple examples. If you already understand RLH (or don’t care!), skip to the next section.]</em></p>
<p>RLH stands for “<em>Root Likelihood</em>” and it expresses the degree to which a fitted model matches observed data from respondents. RLH is usually calculated for observations in the <em>training</em> data (those for whom a model was fit). That’s because those are the observations for which we have individual level estimates.</p>
<blockquote>
<p>Side note: in principle we could calculate RLH for <em>any</em> set of observations given a set of utility scores. For instance, we might calculate how well new observations match the utilities for a particular segment or an overall sample. That is rarely done in my experience, so I’ll just note it and not explore further.</p>
</blockquote>
<p>Let’s break down the term “root likelihood” to see what is going on. First: "<strong>likelihood</strong>”. For a single observation, the <em>likelihood</em> is the odds that the single observation would have occurred, according to our model. For example, suppose we have a MaxDiff set of items A, B, C, and D. Further imagine that we have the following utility scores: Item A is preferred for a particular individual, with a utility of +1.0, while items B, C, and D have utilities +0.5, -0.8, and -1.2 respectively.</p>
<p>Given those utilities, and under the MNL (multinomial logit) share-of-preference formula, the preference share for item A for this respondent, as calculated in R syntax, is <code>exp(1.0) / sum(exp(c(1.0, 0.5, -0.8, -1.2)))</code> or <code>0.53</code>. That means that on a task showing A, B, C, and D, the respondent is estimated to have a 53% chance of choosing their most preferred item, Item A. There leaves an estimated 47% chance they would choose one of the other options (most likely Item B, but potentially C or D).</p>
<p>If the respondent did in fact choose Item A, then the <strong>likelihood</strong> value of that observation is 0.53 (the relevant share of preference for that choice). In this case, the model is doing pretty well because it correctly predicted the most likely choice.</p>
<p>On the other hand, if they chose Item D, then the likelihood — according to the estimated utilities — would be <code>exp(-1.2) / sum(exp(c(1.0, 0.5, -0.8, -1.2)))</code> or a 5.9% chance and a likelihood value of of 0.059. In this case, unlike the odds for Item A above, the model is <em>not</em> doing well because its “agreement” with the actual choice was only 0.059.</p>
<p>In short, the “likelihood” part of the term calculates the odds we should have seen each individual response, according to the model. When the value is higher, it means that the data we saw was more “likely” according to the model.</p>
<p>Next: “<strong>root</strong>”. Given multiple observations, we can calculate their <em>joint likelihood</em> by multiplying the separate odds. For example, suppose that we ask the exact same two tasks in a row as we listed above, and that our respondent chooses A on one, but D on the other task. The likelihood of those two observations together is <code>0.53 * 0.059</code> or <code>0.031</code><em>.</em> But that number is across two trials. To get a single estimate, we take its square <strong>root</strong>, or <code>sqrt(0.53 * 0.059)</code> which is 0.177. That means that the <strong>root likelihood</strong> (RLH) of our two observations, given the estimated model, is RLH = 0.177.</p>
<blockquote>
<p>Side note: you might notice that taking the simple product of multiple likelihood scores assumes that they are probabilities of independent events … but are the events here “independent”? Isn’t it the case that choosing A on one task is highly correlated with the odds of choosing A on another task?</p>
<p>The answer, conceptually speaking, is that dependence across tasks is already included in the model when it estimates the utility scores. Given the fact that we have estimates for A, B, C, D, and so forth, we can now ask, in effect, “what happens when we draw random samples of comparisons?” (the tasks that were given to the respondents). There is no dependence in how those tasks were drawn — they are independent of the model — so the probabilities can be considered independent for this purpose.</p>
<p>To be sure, one might pose higher-order questions about interactions, such as the degree to which the exact task order may influence responses, such that they interact with the model in a non-independent way. However, such questions rapidly become unanswerable — and most likely would have little effect on a model fit score. A general assumption of independence between tasks seems to work well.</p>
</blockquote>
<p>If we have <em>more than two</em> observations, as we usually would in a MaxDiff survey, we just take an appropriate fractional exponent instead of the square root. If we have 10 trials, we would multiply 10 separate odds and then take the 10th root (exponentiate to 1/10) of the product.</p>
<p>In R, such a calculation looks like this:</p>
<pre><code class="lang-r">(individualOdds &lt;- seq(<span class="hljs-number">0.01</span>, <span class="hljs-number">0.99</span>, length=<span class="hljs-number">12</span>))  <span class="hljs-comment"># placeholder for the likelihoods</span>
(rlh &lt;- prod(individualOdds) ^ (<span class="hljs-number">1</span>/length(individualOdds)))
</code></pre>
<p>In the first line of this code, I create a set of fake odds between 0.01 and 0.99, so we will have values to work with. In real data the odds would be calculated from utilities and observations as noted above. The second line is the key point: it computes the root likelihood. First it multiplies all the odds (<code>prod()</code>) and then takes the Xth root of them (<code>^(1/length(individualOdds))</code>).</p>
<blockquote>
<p><strong>Side note about that calculation using</strong> <code>prod()</code> for repeated multiplication: this is the <em>didactic &amp; conceptual</em> way to calculate RLH. See the code below for a <em>better computational version</em>.</p>
</blockquote>
<p>In case you’re wondering … <strong>no, you don’t have to calculate RLH on your own</strong> from the data. A platform like Sawtooth Software Discover or Lighthouse Studio will calculate RLH and include it automatically in the individual level data. I’m showing the calculations only for one reason: so you will know how it works!</p>
<p>By the way, <strong>RLH works exactly the same way for conjoint analysis</strong>. After estimating a conjoint utility model, we can calculate the odds of each observed choice based on those utilities, and then take the Xth root to calculate RLH.</p>
<hr />
<h2 id="heading-what-you-can-do-with-rlh">What You Can Do with RLH</h2>
<p>In general, RLH can be used in two ways:</p>
<ul>
<li><p>As a <strong>high level signal of quality</strong>, namely whether your data are markedly <em>suspect</em></p>
</li>
<li><p>As a way to <strong>filter out potentially non-informative respondents</strong></p>
</li>
</ul>
<p>As for quality, I <em>don’t</em> say that RLH indicates whether a data set or a respondent is “good.” No single metric can assess that. However, if RLH is markedly low — as we explore below — then it indicates potentially serious problems. In particular, if the median RLH in a sample falls below a reasonable threshold (keep reading) then you have a problem such as inconsistent respondents, a poor survey, or a poor model.</p>
<p>Beyond the high level diagnostic value, some analysts use RLH to filter out so-called “bad” respondents. Typically this is done by setting a minimum threshold for an acceptable RLH (again, keep reading!), removing respondents who are below that value, and then estimating the model again without them.</p>
<p>In practice, <strong>I almost never remove respondents due to RLH</strong> for four reasons:</p>
<ol>
<li><p>RLH does <strong>not</strong> identify whether responses are “bad”. It only says how <em>likely</em> they are, conditional on the model that was estimated from them. I discuss this below at some length, near the end of this post.</p>
</li>
<li><p>Instead of removing respondents, I try instead to <strong>use high quality data sources</strong>. I don’t believe analytics can “rescue” bad data sources.</p>
</li>
<li><p><strong>Choice models are robust</strong> to random answers, straight lining, speeding, and similar “bad” response patterns. So, if even a modestly large number of respondents are answering in those ways, it is unlikely to seriously affect the ultimate estimates (at least at the level of sample averages).</p>
</li>
<li><p>Any method to remove “bad” respondents <strong>introduces other sources of bias</strong>. As one minor example: why not remove excessively high RLH respondents who are “too good”? Again, I say more below.</p>
</li>
</ol>
<p>A <strong>counterargument</strong> to my position is the following: if a respondent’s RLH is near the value of “random” observations, then we are not learning much from them, so we might as well remove them from the model to be more precise and efficient. <em>For reasons I discuss below, that is not the complete story</em> and there is more to consider. We’ll get to that!</p>
<p>Meanwhile, before acting on RLH, we need to inspect it. We’ll do that next.</p>
<hr />
<h2 id="heading-examining-rlh-in-the-uxr-maxdiff-data">Examining RLH in the UXR MaxDiff Data</h2>
<p>Now that you know how RLH works and what you can do with it, let’s look at the values from the <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-1-data-averages">Quant UX Association MaxDiff</a> for N=308 respondents. Sawtooth’s data file helpfully includes the individual RLH values when it exports individual level data, so you don’t have to calculate anything.</p>
<p>Using the data set loaded above, we can set a friendly column name and start exploring:</p>
<pre><code class="lang-r">names(md.dat)[<span class="hljs-number">2</span>] &lt;- <span class="hljs-string">"RLH"</span>
summary(md.dat$RLH)
</code></pre>
<p>In our data, the MaxDiff RLH values range from 0.32 to 0.80, with a median of 0.60:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733946480229/743b6a78-34e7-4710-8113-15f5996b93bd.png" alt class="image--center mx-auto" /></p>
<p>As always, it’s important to plot the values. The <code>ggridges</code> package makes nice density plots with the <code>geom_density_ridges()</code> function, including an option to fill in the individual points (<code>jittered</code> so they don’t overlap under the curve):</p>
<pre><code class="lang-r"><span class="hljs-comment"># density plot of the RLH values</span>
<span class="hljs-keyword">library</span>(ggplot2)
<span class="hljs-keyword">library</span>(ggridges)
set.seed(<span class="hljs-number">98101</span>)    <span class="hljs-comment"># jittered points are slightly randomized</span>
p &lt;- ggplot(data=md.dat, aes(x=RLH, y=<span class="hljs-number">1</span>)) +
  geom_density_ridges(jittered_points=<span class="hljs-literal">TRUE</span>, 
                      alpha=<span class="hljs-number">0.5</span>, colour=<span class="hljs-string">"darkblue"</span>) +
  xlab(<span class="hljs-string">"Model fit score (RLH; &gt; 0.3 is good)"</span>) +
  ylab(<span class="hljs-string">"Relative prevalence (density)"</span>) +
  theme_minimal()

p
</code></pre>
<p>In the code above, I set <code>y=1</code> because <code>ggridges</code> generally assumes that you have multiple series to plot overlapping density curves, identified with a nominal y variable. In this case, we have only one series, so I set a single dummy y value.</p>
<p>Here’s the result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733946669522/bc756907-8cd6-48e7-83da-100b43246acb.png" alt class="image--center mx-auto" /></p>
<p>How do we interpret this chart? Well, I’ve already labelled the X axis with “<em>&gt; 0.3 is good</em>” … but the reason for that label will have to wait until the next section.</p>
<p>Accepting that cutoff value for a moment, we see a few things in the chart:</p>
<ul>
<li><p><strong>All of the individual fit values are “good”</strong> according to the purported cutoff value of 0.3.</p>
</li>
<li><p>The curve is <strong>nicely Gaussian (normal)</strong> in shape, which is a good thing … for reasons I won’t go deeply into. (<em>Hint</em>: it relates to assumptions of the HB model as well as random tasks, and assumptions of random variation across people.)</p>
</li>
<li><p>There is a small <strong>elevated tail on the lower end</strong> with RLH &lt; 0.4. Those come from 2% of respondents for whom the model didn’t fit as well. We don’t know exactly why not, but perhaps they sped through it, or were distracted, or didn’t find the items or topic as relevant as other respondents did.</p>
</li>
</ul>
<p>So we’ve seen the results and understood how they are calculated. <strong>But what is a “good” value for RLH?</strong> Is it OK that our median is 0.60? Why not 0.30 or 0.90?</p>
<p>In the next section, I discuss the threshold levels that make RLH “good” or not.</p>
<hr />
<h2 id="heading-are-those-values-good-or-bad-what-should-we-expect">Are Those Values Good or Bad? What should we Expect?</h2>
<p>In this section, I briefly discuss 2 heuristics that build intuition around RLH expectations. In following sections, I’ll look at a code-based analysis in R; and then mention another approach suggested by Sawtooth that uses random data.</p>
<p>For the <strong>first heuristic</strong>, a “good” value for RLH will be substantially <em>better than random chance</em>. For a MaxDiff (or Conjoint) task, <strong>one way</strong> to consider random chance would be if every item had the same likelihood of being chosen. For example, if a MaxDiff task has C=5 items on each screen, then we might say that each one has a random likelihood of 1/C = 1/5 = 0.20.</p>
<p>Put differently, our model should get 20% of the choices right merely by picking an item from 1-5 randomly. We want to do substantially better than that … let’s say we want to do “50% better.” That’s a heuristic cutoff but agrees with a lot of experience. <strong>Then we could set a minimum likelihood of RLH = 0.30 to be better than that form of random chance</strong>.</p>
<p>For the <strong>second heuristic</strong>, we note that the one above is a somewhat simplistic version of random chance. We might instead say, “we know that items will vary in preference, so there’s no way they will all have 1/C odds.” A different way to look at random chance incorporates that concept. This will help us look not at the <em>minimum</em> acceptable RLH (as above) but at what we might expect for a <em>good</em> RLH value.</p>
<p>Suppose we assume that 5 items will have relative preference of (1, 2, 3, 5, 8). I’m making that up (using the Fibonacci sequence) just as a heuristic example. However, it’s not an unreasonable set because real world results often find 1-3 items that are “winners” plus a long tail of less-preferred items.</p>
<p>In R, we could calculate the RLH expected given those odds. We will <em>assume</em> that the model knows those odds that they perfectly match the item chosen on the tasks (i.e., that the respondent chose the item that aligns with the most-likely item). Here’s the code in R:</p>
<pre><code class="lang-r"><span class="hljs-comment"># back of the envelope</span>
<span class="hljs-comment"># pick some preference distribution and calculate odds</span>
itemOdds &lt;- c(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">5</span>, <span class="hljs-number">8</span>)           <span class="hljs-comment"># relative preference for 5 items</span>
itemOdds &lt;- itemOdds / sum(itemOdds)   <span class="hljs-comment"># rescale so they sum to 1.0</span>
max(itemOdds)          <span class="hljs-comment"># relative odds for the most-preferred item</span>
</code></pre>
<p>That is a value of <code>itemOdds = 0.42</code>. So — under this one set of preferences, a model that correctly predicted the choice of the most-preferred item — would have a likelihood or RLH of 0.42. That suggests heuristically that <strong>an RLH of approximately 2/C (2/5 = 0.40) may be pretty good</strong>.</p>
<blockquote>
<p>BTW, you might note that this also means that a perfect model, given those preferences, would be wrong in its predictions ~60% of the time. We can’t expect our models to be extremely accurate about point predictions exactly because people’s preferences are tendencies — they are not 0/1 values. For example, someone who would register for one class may also have a high chance of registering for another one. Choice models are helpful because they tell us what is more or less likely, on average, and how options compare to one another … not because they exactly predict individual events.</p>
</blockquote>
<p>Putting this value together with the previous minimum heuristic, where 1.5/C may be a lower cutoff, we could form a range bounded like this:</p>
<ul>
<li><p><strong>Rough cutoff</strong>: &lt; approx 1.5/C. For example, with 5 items shown at a time: 1.5/5 = 0.30. For 4 items shown at a time: 1.5/4 = approx 0.375 as a lower minimum.</p>
</li>
<li><p><strong>Clearly good</strong>: = approx 2/C. For example, with 5 items: 2/5 = approx 0.40.</p>
</li>
</ul>
<p>In other words, RLH is heuristically “good” when it is in or above the range of (0.30 — 0.40) when we test 5 items at a time.</p>
<p>Are you thinking, “Wait! That’s a pretty low value. I would have expected a good model to have RLH of 0.80 or 0.90 or 0.99!”</p>
<p>You’re right — it is not a high value. We’ll see more about that below. Meanwhile, it is always important to remember that <strong>respondents usually find multiple items to be interesting</strong>, and might prefer one or another more or less randomly. For example, I like Dr. Pepper Zero soda … but a model of my preference is not 100% Dr. Pepper! I often drink other things, especially water, but also coffee, Gatorade, tea, Zevia soda, and so forth. That means <strong>we should not expect a model to do any better than to capture my most likely choice, at the level of its relative share</strong> — a likelihood much lower than 1.0.</p>
<p><strong>Key point: these heuristic results should help you build intuition about why RLH will not be especially high in real data</strong>.</p>
<p>You might wonder next, “<em>Can we quantify that better, not just using a thought exercise?</em>” We’ll do that systematically in the next section.</p>
<hr />
<h2 id="heading-deep-dive-r-code-to-simulate-rlh-values">Deep Dive: R Code to Simulate RLH Values</h2>
<p><em>This section is a moderately deep dive into R simulation! If you are satisfied with the heuristics in the previous section, you can skip it … unless you like R as much as I do.</em></p>
<p>The previous section described why RLH in the range of RLH &gt; roughly 1.5/C — which is to say, 0.30 for a MaxDiff with 5 items shown at a time — may be a reasonable minimum cutoff for a good respondent using heuristic logic. We also saw that RLH ~ 2/C — or 0.40 for 5 items — may indicate a particularly good fit between the observations and model. Now <strong>we’ll examine the expectation more systematically using code</strong>.</p>
<p>Here’s what I’m going to do. The short version is that I’ll do many, many random iterations of the kind of heuristic I showed above, for different sets of preference values. That will inform us as to the generally expected range of RLH values when the observations and data match perfectly.</p>
<p>More specifically, the code will simulate “respondent utilities and choice” as follows:</p>
<ul>
<li><p><strong>Draw utilities as random normal values</strong> with mean=0 and some standard deviation (I’ll come back to that). This mirrors the assumptions of the HB models used to estimate MaxDiff utilities, so the values will be realistic.</p>
</li>
<li><p><strong>Draw random sets of “items” from those</strong>, 5 at a time, to mimic the utilities that might apply to a particular MaxDiff task.</p>
</li>
<li><p>For each set of 5 item utilities, <strong>calculate the share of preference for the single most-preferred item</strong>. That gives us the highest possible likelihood value for that task (if the respondent always chose the most-preferred item, according to the model)</p>
</li>
<li><p>Do the above using <strong>K [“nItems”] = 14 items</strong> (because that matches the <em>number of items</em> on the Quant UX survey that provided our data)</p>
</li>
<li><p>For each simulated “respondent”, do this for <strong>C [“nShown”] = 5 items</strong> at a time on each screen, for <strong>S [“nScreens”] = 6 total screens</strong> (because that matches the <em>length</em> of the Quant UX survey).</p>
</li>
<li><p>Simulate all of the above a total <strong>N [“nIter”]=10000 times</strong> (“respondents”)</p>
</li>
</ul>
<p>Here is code that sets up the initial conditions for all of that. It also sets a random number seed to make the analysis repeatable:</p>
<pre><code class="lang-r">nIter    &lt;- <span class="hljs-number">10000</span>            <span class="hljs-comment"># how many times to sample nItem MaxDiff simulations</span>
nItems   &lt;- <span class="hljs-number">14</span>               <span class="hljs-comment"># number of items in a MaxDiff set</span>
nShown   &lt;- <span class="hljs-number">5</span>                <span class="hljs-comment"># number of items shown on a single screen</span>
nScreens &lt;- <span class="hljs-number">6</span>                <span class="hljs-comment"># number of screens in our survey</span>
rlhDraws &lt;- rep(<span class="hljs-literal">NA</span>, nIter)   <span class="hljs-comment"># hold the results for each iteration</span>
set.seed(<span class="hljs-number">98250</span>)              <span class="hljs-comment"># make it repeatable</span>
</code></pre>
<p><em>Technical note</em>: although MaxDiff utilities are always zero-centered (in their “raw” form estimated by an HB model), utilities vary from study to study and item to item in their <em>standard deviation</em> (SD). To draw random zero-centered, simulated utilities, we need to specify that variance. To get an SD value for this simulation, I look empirically at the Quant UX data and use the median SD (<code>drawSD</code>) for the items in that study:</p>
<pre><code class="lang-r">summary(unlist(lapply(qux.md[ , classCols], sd)))   <span class="hljs-comment"># answer: somewhere around sd ==(2.2, 2.6)</span>
(drawSD  &lt;- median(unlist(lapply(qux.md[ , classCols], sd))))
</code></pre>
<p>Now we’re all set up and ready to run the simulation. Following is the code. First I’ll give the whole chunk, and then I’ll discuss it below.</p>
<pre><code class="lang-r"><span class="hljs-keyword">for</span> (i <span class="hljs-keyword">in</span> <span class="hljs-number">1</span>:nIter) {
  <span class="hljs-comment"># set up 1 respondent "trial"</span>
  pwsResp     &lt;- rnorm(nItems, mean=<span class="hljs-number">0</span>, sd=drawSD) <span class="hljs-comment"># "nItems" random normal, zero-centered simulated part worths</span>
  pwsResp     &lt;- scale(pwsResp, scale=<span class="hljs-literal">FALSE</span>) <span class="hljs-comment"># recenter to make sure they're zero-sum</span>

  <span class="hljs-comment"># iterate over nScreen tasks per respondent</span>
  drawMax &lt;- rep(<span class="hljs-literal">NA</span>, nScreens*<span class="hljs-number">2</span>)             <span class="hljs-comment"># hold the results across the screens for one respondent</span>
  <span class="hljs-keyword">for</span> (j <span class="hljs-keyword">in</span> <span class="hljs-number">1</span>:nScreens) {
    pwsDrawn    &lt;- sample(pwsResp, nShown)   <span class="hljs-comment"># get the part worths for 1 simulated task  </span>
    <span class="hljs-comment"># "best" choice</span>
    pwsExp      &lt;- exp(pwsDrawn)             <span class="hljs-comment"># exponentiate those part worths</span>
    pwsMaxShare &lt;- max(pwsExp) / sum(pwsExp) <span class="hljs-comment"># our best (most likely) prediction would be the max utility item</span>
    drawMax[j*<span class="hljs-number">2</span> - <span class="hljs-number">1</span>]  &lt;- pwsMaxShare
    <span class="hljs-comment"># repeat for the "worst" choice</span>
    pwsExp        &lt;- exp(-<span class="hljs-number">1</span> * pwsDrawn)      <span class="hljs-comment"># exponentiate part worths for the "worst" choice direction</span>
    pwsMaxShare   &lt;- max(pwsExp) / sum(pwsExp)   
    drawMax[j*<span class="hljs-number">2</span>]  &lt;- pwsMaxShare
  }
  <span class="hljs-comment"># calculate RLH from those observations</span>
  rlh         &lt;- exp(sum(log(drawMax[drawMax &gt; <span class="hljs-number">0</span>]), na.rm=<span class="hljs-literal">TRUE</span>) / length(drawMax))    <span class="hljs-comment"># safer equivalent of "prod(drawMax) ^ (1 / length(drawMax))"</span>
  rlhDraws[i] &lt;- rlh
  <span class="hljs-comment"># add a bit of error checking, just in case of an off by one error etc :-/</span>
  <span class="hljs-keyword">if</span> (length(drawMax) != nScreens * <span class="hljs-number">2</span>) <span class="hljs-keyword">warning</span>(<span class="hljs-string">"The vector of partworths is off somewhere!"</span>, i, <span class="hljs-string">":"</span>, drawmax)
}
</code></pre>
<p>This code has five main parts:</p>
<ol>
<li><p>An outer loop <strong>iterates over the 10000 (</strong><code>nIters</code><strong>) simulated “respondents”</strong> (Line 1). For each of those iterations:</p>
<ol>
<li><p>It <strong>draws zero-centered utilities</strong> for the 14 items (Lines 3-4) for that 1 simulated respondent</p>
</li>
<li><p>An inner loop <strong>iterates over 6 MaxDiff choice “trials”</strong> (Line 8), and inside that:</p>
<ol>
<li><p>It makes a <strong>trial with 5 randomly chosen items</strong> to be compared on 1 choice task (Line 9)</p>
</li>
<li><p>It <strong>chooses the most likely Best &amp; Worst</strong> on that random trial and calculates the likelihoods for those choices (Lines 11-17).</p>
</li>
<li><p>When it’s done with the 6 tasks, <strong>it calculates RLH for the set</strong> of 12 observed Best &amp; Worst “choices” for that respondent (6 Best + 6 Worst), and saves that result (Lines 20-21)</p>
</li>
</ol>
</li>
</ol>
</li>
</ol>
<p>The error check line at the end was just some defensive coding that occurred to me along the way (nothing in particular prompted it; I just like to code defensively). It never triggered.</p>
<blockquote>
<p><em>Side note</em>: this code assumes that the overall, upper level means of all items are also zero. One could instead structure those; that’s a longer discussion. A more general approach would be to iterate over different sets of part worth scores; different survey lengths; and different sizes of choice sets. The code above could serve as the basis for a function that is refactored for use inside a larger and more general set of loops and/or parameter variations. That would be a good exercise!</p>
</blockquote>
<p>BTW, you will see that <strong>RLH here is computed with a different formula</strong> than above (but one that is mathematically equivalent). Why? Because, when many odds are multiplied together, they will quickly underflow real number calculation engines. By taking the <code>log()</code> of them and then <em>adding</em> the logs rather than multiplying, we avoid that problem.</p>
<blockquote>
<p>Side note: R can handle multiplying 12 probabilities — but if the survey were longer, then we could have problems. It’s always good to anticipate such things and proactively avoid them when coding!</p>
</blockquote>
<hr />
<h2 id="heading-results-from-the-simulation">Results from the Simulation</h2>
<p>After running this code, we have RLH scores for 10000 simulated “respondents”. As a reminder, the code has assumed that all of the “observed” choices aligned <em>perfectly</em> with the most likely, most preferred items, according to the simulated individual level utilities. <strong>Thus it sets an <em>upper bound</em> for expectations of RLH</strong>. (<em>Caveat</em>: this assumes there is no “none of the above” option. I discuss that below.)</p>
<p>Here’s code to plot the results, slightly adapting the density chart code from above:</p>
<pre><code class="lang-r"><span class="hljs-comment"># density plot</span>
set.seed(<span class="hljs-number">98195</span>)    <span class="hljs-comment"># jittered points are slightly randomized</span>
p &lt;- ggplot(data=as.data.frame(rlhDraws), aes(x=rlhDraws, y=<span class="hljs-number">1</span>)) +
  geom_density_ridges(jittered_points=<span class="hljs-literal">TRUE</span>, 
                      alpha=<span class="hljs-number">0.1</span>, colour=<span class="hljs-string">"darkblue"</span>) +
  xlab(<span class="hljs-string">"Model fit score (RLH; &gt; 0.3 is good)"</span>) +
  ylab(<span class="hljs-string">"Relative prevalence (density)"</span>) +
  ggtitle(<span class="hljs-string">"RLH Simulation, Perfectly Made Choices (N=10000)"</span>) +
  theme_minimal()

p
</code></pre>
<p>And the resulting chart:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734724305501/128c83d6-98a5-4ecb-9567-c6cf860e30f2.png" alt class="image--center mx-auto" /></p>
<p>This tells us that when respondents make choices perfectly in alignment with their estimated preferences — with other assumptions about utility scores and survey parameters, as above — we can expect those <strong>upper-bound RLH values</strong> to fall between roughly 0.4 and 0.9, with most falling between 0.5 and 0.8.</p>
<p>In other words, if we have no other information except that a respondent shows RLH=0.5 or RLH=0.4, it could in fact be a maximum, perfect RLH score for them. I<strong>n general, we should not expect — and instead, should be <em>surprised</em>, because it is near “perfect” — to see RLH &gt;= 0.6</strong> or so.</p>
<p>As we saw above, our actual UXR data had median RLH = 0.60. That suggests that our respondents are not too far below “perfect” in their response patterns.</p>
<p>Put differently, UXRs are great respondents when a survey is relevant and motivating for them! (In a consumer sample, I would expect substantially lower RLH, more like a median of 0.40.)</p>
<p>We can compare the two sets of results — actual vs. UXR — using the same kind of density plot. Here’s the code:</p>
<pre><code class="lang-r"><span class="hljs-comment"># combined density plot (using random subset of simulated data)</span>
compare.df &lt;- data.frame(RLH    = c(md.dat$RLH, rlhDraws),
                         Source = rep(c(<span class="hljs-string">"UXR"</span>, <span class="hljs-string">"Sim"</span>), 
                                      times=c(length(md.dat$RLH), 
                                              length(rlhDraws))))

<span class="hljs-comment"># density plot</span>
set.seed(<span class="hljs-number">98102</span>)    <span class="hljs-comment"># jittered points are slightly randomized</span>
p &lt;- ggplot(data=compare.df, aes(x=RLH, y=Source, color=Source)) +
  geom_density_ridges(jittered_points=<span class="hljs-literal">TRUE</span>, 
                      alpha=<span class="hljs-number">0.1</span>) +
  xlab(<span class="hljs-string">"RLH"</span>) +
  ylab(<span class="hljs-string">"Relative prevalence (density)"</span>) +
  ggtitle(<span class="hljs-string">"RLH Comparison: UXR Data vs. 'Perfect' Simulation"</span>) +
  theme_minimal()

p
</code></pre>
<p>In this code, I first combine the two data sources into a single data set. Then I use the same density plot functions, adding the “Source” of each data point (UXR data vs. simulation) as the y variable, which will stack the results as two density curves.</p>
<p>Here’s the chart:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734725377728/c9b82018-d2bb-4a28-a798-71a2f834216e.png" alt class="image--center mx-auto" /></p>
<p>As you can see, the UXR data is pretty close to “perfect.” Again, I would not expect this in most data sets! Usually I would be happy with median RLH &gt; 0.4 or so. But this comparison <strong>highlights once more what we might expect as the upper bound</strong> of reasonable RLH.</p>
<p>Looking a bit further, we see an interesting suggestion of tri-modality in the UXR distribution of RLH. On the <em>right hand side</em> (peaking ~0.65), there is a suggestion of a group that looks exactly like the “perfect” data. Then there is a <em>middle peak</em> suggesting a group (peaking ~0.60) that looks slightly less than perfect. Finally, there is a <em>left hand peak</em> (peaking ~ 0.52) that shows lower RLH for some reasons (there are many) — which is not at all “bad” by any means, but contrasts the two higher modal peaks.</p>
<p>Overall I wouldn’t make much of that observation; I’m just highlighting it as a signal of potential additional questions we might have about respondent style, sources, quality, etc., in a sample.</p>
<hr />
<h2 id="heading-another-approach-rlh-cutoffs-via-random-data">Another Approach: RLH Cutoffs via Random Data</h2>
<p>Another approach to using RLH as a cutoff comes from the choice modeling experts at Sawtooth (<a target="_blank" href="https://content.sawtoothsoftware.com/assets/48af48f3-c01e-42ff-8447-6c8551a6d94f">Orme, 2019</a> PDF). They noted two things:</p>
<ol>
<li><p>RLH for “random” responses can vary on a given survey due to the details of the survey’s experimental design (e.g., some tasks are “harder” or “easier” relative to the model overall)</p>
</li>
<li><p>Their platform allows creation of random “respondents” who take the actual survey.</p>
</li>
</ol>
<p>Putting those together, they suggested using the random respondent method in their platform to empirically determine a point at which the RLH for a particular respondent is clearly below the expectation for a random respondent. Specifically, identify a percentile cutoff, such as the 95th percentile (highest 5%) of RLH in the simulated random respondents. After identifying that point, filter out respondents below that; and then run the overall model again without them.</p>
<p>For step by step details, see the excellent white paper (<a target="_blank" href="https://content.sawtoothsoftware.com/assets/48af48f3-c01e-42ff-8447-6c8551a6d94f">Orme, 2019</a> PDF). Unlike the heuristic R code above, this method exactly fits your own survey. (BTW, that method could be used in any platform that offers random responses plus individual RLH calculation.)</p>
<p>However, I will also note that <strong>one should be cautious about filtering</strong> — as Orme notes in the white paper and as I will discuss next.</p>
<hr />
<h2 id="heading-caveats-rlh-fit-values-and-filtering-responses-for-data-quality">Caveats: RLH Fit Values and Filtering Responses for “Data Quality”</h2>
<p>Before we finish, I want to clarify two things: that <strong>RLH is not about respondents</strong> but is about the <em>fit</em> between observations and model; and that <strong>filtering respondents with any method is risky</strong>.</p>
<h3 id="heading-rlh-is-not-about-respondent-quality"><strong><em>RLH is not about respondent quality</em></strong></h3>
<p>Throughout the post, I’ve described how RLH relates to the mutual likelihood of a model and observations. I want to be very clear that <em>this does not necessarily say anything about respondent quality</em>.</p>
<p>Why not? Consider an edge case (actually somewhat common) when <strong>a respondent accurately reports that they do not like any of the items</strong> on a MaxDiff. Perhaps they are completely uninterested in the features or project … and they truthfully and diligently report that disinterest. Suppose that the model accurately says that their utilities are ~0 (±error) for every item.</p>
<p>In that case, the RLH for this diligent respondent on a C=5 items-at-a-time survey would approximate <code>exp(0) / (5*exp(0))</code> = 1/C … indistinguishable from the expectation for a completely random respondent!</p>
<p>That means that <em>low RLH is only a signal that something is wrong</em>. It may be that the respondents are answering randomly. Yet it may be that your items are unappealing, or that you are targeting the wrong audience, and respondents are accurately communicating that. <strong>In this case, filtering out low RLH respondents could be the exact wrong thing to do</strong>, because their disinterest may be important to know.</p>
<p>That doesn’t mean that RLH is useless. Rather, <strong>RLH is one diagnostic signal, and needs to be placed into context</strong> with other signals (such as observations from an in-person pilot study, which I always advocate).</p>
<blockquote>
<p>BTW, RLH can also be exceptionally <strong>high</strong> for similar reasons — which may happen in particular when a conjoint survey or MaxDiff has a “none” option. If a respondent answers “no, I don’t want it” on every task, then their “none” utility will have a very large value.</p>
<p>Suppose a respondent’s “none” utility = 5.0, while 10 other items each have utility = -0.5. Then, on a C=5 task including “none”, their expected RLH = <code>exp(5) / (4*exp(-0.5) + exp(5))</code> = 0.98.</p>
<p>That might indicate a “bad” respondent who simplifies the survey by picking “none” every time. OTOH, it might be a good respondent who accurately and importantly says that they don’t want the product. Again, the point is not that RLH identifies whether any respondent is good or bad. Rather, it must be used diagnostically with other knowledge and signals.</p>
</blockquote>
<h3 id="heading-filtering-respondents-is-risky"><strong><em>Filtering respondents is risky</em></strong></h3>
<p>I say this every chance I get, and won’t belabor it: anytime we filter respondents — whether that is from a screener, a model fit estimate, or whatever — we are introducing bias into the sample and its results.</p>
<p><strong>The worst offenders that I see in this are are surveys that use multiple screening items</strong> to identify the “right” audience. For instance, they might want to find “<em>intenders</em> in the <em>next 3 months</em> in our <em>target audience</em> who are <em>familiar</em> with the product category and <em>already use</em> at least one product in the category.”</p>
<p><strong>If a survey does that, stop and reconsider</strong>! Each of the 5 conditions in that hypothetical screener adds error. Unless the items have been psychometrically evaluated for prevalence, validity, and relation to the constructs of interest, then I would assume that filtering is <em>adding</em> a large and unknown degree of error, not reducing it!</p>
<p>The same considerations apply to filtering due to RLH as I described above. Filtering by RLH might reduce non-informative respondents … yet it also might remove respondents who are telling you something vitally important about their own disinterest. It’s important to know <em>when</em> RLH is low, because you can follow up by exploring <em>why</em> it is low.</p>
<hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>I’ll finish with my recommended sequence of considerations for RLH with a choice modeling survey (MaxDiff or Conjoint Analysis):</p>
<ol>
<li><p><strong>Always examine — through descriptive stats and plotting — the individual-level RLH estimates</strong> for your dataset in a choice model survey. Not just the overall average, but the individuals.</p>
</li>
<li><p>If the curve is <strong>approximately normal, and only a small % are below a cutoff of 1.5/C</strong> (e.g., 1.5 / 5 items = 0.3 for a 5-item-at-a-time MaxDiff) … then I’d say RLH looks good, and don’t worry further about it. The UXR data above is an example of this. (Yay! UXRs are excellent respondents.)</p>
</li>
<li><p>OTOH, <strong>if (say) &gt;10% of respondents have RLH &lt; 1.5/C, then you need to identify why</strong> that is occurring. The reasons might reflect any or all of these:</p>
<ol>
<li><p><em>Accurate responses</em> of disinterest that are important to know</p>
</li>
<li><p>A <em>problem with the survey’s</em> <em>content</em>, concept phrasing, programming, etc.</p>
</li>
<li><p>The <em>wrong target audience,</em> perhaps indicating a need for new sample</p>
</li>
<li><p><em>Low quality respondents</em> due to response styles or panel characteristics</p>
</li>
<li><p><em>Poor model fit for other reasons</em>, such as inappropriate constraints on estimation</p>
</li>
</ol>
</li>
<li><p><strong>There is no way to distinguish those five possibilities for low RLH without additional information</strong>, such as other signals, qualitative pre-testing of the survey, reconsideration of any unusual model choices, and so forth.</p>
</li>
<li><p>Similarly, but less commonly, if your survey includes a “none” option, look out for an <em>unusually high RLH peak</em> (a moderate or high % of respondents with RLH &gt; 0.8 or so).</p>
</li>
</ol>
<p><strong>In short, don’t use RLH as an automatic filter!</strong> Use it as a way to <em>understand more about your data</em> and to identify potential questions about respondents’ response styles.</p>
<p>I hope this discussion and the R code above will help you to use RLH with your data. Cheers!</p>
<hr />
<h2 id="heading-all-the-r-code">All the R Code</h2>
<p>As always, following is the R code for all of the blocks above, in one place. [BTW, the references to “12.x” are there because this code is continuous with code from the previous 3 posts of this series.]</p>
<pre><code class="lang-r"><span class="hljs-comment">##### 12. Data Quality / RLH</span>

<span class="hljs-comment"># 12.1</span>
<span class="hljs-comment"># get the data; repeating here for blog post 4, see post 1 for details</span>
<span class="hljs-keyword">library</span>(openxlsx)   <span class="hljs-comment"># install if needed, as with all package calls</span>
md.dat &lt;- read.xlsx(<span class="hljs-string">"https://quantuxbook.com/misc/QUX%20Survey%202024%20-%20Future%20Classes%20-%20MaxDiff%20Individual%20raw%20scores.xlsx"</span>)   <span class="hljs-comment"># </span>
md.dat$Anchor &lt;- <span class="hljs-literal">NULL</span>
names(md.dat)[<span class="hljs-number">3</span>:<span class="hljs-number">16</span>] &lt;- c(<span class="hljs-string">"Choice Models"</span>,  <span class="hljs-string">"Surveys"</span>,       <span class="hljs-string">"Log Sequences"</span>,    <span class="hljs-string">"Psychometrics"</span>, 
                         <span class="hljs-string">"R Programming"</span>,  <span class="hljs-string">"Pricing"</span>,       <span class="hljs-string">"UX Metrics"</span>,       <span class="hljs-string">"Bayes Stats"</span>,
                         <span class="hljs-string">"Text Analytics"</span>, <span class="hljs-string">"Causal Models"</span>, <span class="hljs-string">"Interviewer-ing"</span>,  <span class="hljs-string">"Advanced Choice"</span>, 
                         <span class="hljs-string">"Segmentation"</span>,   <span class="hljs-string">"Metrics Sprints"</span>)
classCols &lt;- <span class="hljs-number">3</span>:ncol(md.dat)    <span class="hljs-comment"># generally, Sawtooth exported utilities start in column 3</span>

<span class="hljs-comment"># simple calculations as described in the post</span>
exp(<span class="hljs-number">1.0</span>) / sum(exp(c(<span class="hljs-number">1.0</span>, <span class="hljs-number">0.5</span>, -<span class="hljs-number">0.8</span>, -<span class="hljs-number">1.2</span>)))  <span class="hljs-comment"># item A</span>
exp(-<span class="hljs-number">1.2</span>) / sum(exp(c(<span class="hljs-number">1.0</span>, <span class="hljs-number">0.5</span>, -<span class="hljs-number">0.8</span>, -<span class="hljs-number">1.2</span>))) <span class="hljs-comment"># item D       </span>
<span class="hljs-number">0.53</span> * <span class="hljs-number">0.059</span>
sqrt(<span class="hljs-number">0.53</span> * <span class="hljs-number">0.059</span>)

(individualOdds &lt;- seq(<span class="hljs-number">0.01</span>, <span class="hljs-number">0.99</span>, length=<span class="hljs-number">12</span>))  <span class="hljs-comment"># placeholder for the likelihoods</span>
(rlh &lt;- prod(individualOdds) ^ (<span class="hljs-number">1</span>/length(individualOdds)))

<span class="hljs-comment"># 12.2 RLH</span>
names(md.dat)[<span class="hljs-number">2</span>] &lt;- <span class="hljs-string">"RLH"</span>
summary(md.dat$RLH)

<span class="hljs-comment"># let's look at our UXR data</span>
<span class="hljs-comment"># density plot of the RLH values</span>
<span class="hljs-keyword">library</span>(ggplot2)
<span class="hljs-keyword">library</span>(ggridges)
set.seed(<span class="hljs-number">98101</span>)    <span class="hljs-comment"># jittered points are slightly randomized</span>
p &lt;- ggplot(data=md.dat, aes(x=RLH, y=<span class="hljs-number">1</span>)) +
  geom_density_ridges(jittered_points=<span class="hljs-literal">TRUE</span>, 
                      alpha=<span class="hljs-number">0.5</span>, colour=<span class="hljs-string">"darkblue"</span>) +
  xlab(<span class="hljs-string">"Model fit score (RLH; &gt; 0.3 is good)"</span>) +
  ylab(<span class="hljs-string">"Relative prevalence (density)"</span>) +
  theme_minimal()

p

<span class="hljs-comment"># 12.3</span>
<span class="hljs-comment"># how well might we *expect* to predict, if respondents answers agree *perfectly* with an estimated model?</span>

<span class="hljs-comment"># back of the envelope</span>
<span class="hljs-comment"># pick some preference distribution and calculate odds</span>
itemOdds &lt;- c(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">5</span>, <span class="hljs-number">8</span>)           <span class="hljs-comment"># relative preference for 5 items</span>
itemOdds &lt;- itemOdds / sum(itemOdds)   <span class="hljs-comment"># rescale so they sum to 1.0</span>
max(itemOdds)          <span class="hljs-comment"># relative odds for the most-preferred item</span>

<span class="hljs-comment"># code version</span>
<span class="hljs-comment"># we can do much better with a simulation model!</span>
nIter    &lt;- <span class="hljs-number">10000</span>            <span class="hljs-comment"># how many times to sample nItem MaxDiff simulations</span>
nItems   &lt;- <span class="hljs-number">14</span>               <span class="hljs-comment"># number of items in a MaxDiff set</span>
nShown   &lt;- <span class="hljs-number">5</span>                <span class="hljs-comment"># number of items shown on a single screen</span>
nScreens &lt;- <span class="hljs-number">6</span>                <span class="hljs-comment"># number of screens in our survey</span>
rlhDraws &lt;- rep(<span class="hljs-literal">NA</span>, nIter)   <span class="hljs-comment"># hold the results for each iteration</span>
set.seed(<span class="hljs-number">98250</span>)              <span class="hljs-comment"># make it repeatable</span>

<span class="hljs-comment"># we have to set distribution parameters for our simulated part worths</span>
<span class="hljs-comment"># in HB model, they are random normal, mean=0, ... but what sd should we use?</span>
<span class="hljs-comment"># look at empirical data to pick a reasonable sd</span>
summary(unlist(lapply(qux.md[ , classCols], sd)))   <span class="hljs-comment"># answer: somewhere around sd ==(2.2, 2.6)</span>
(drawSD  &lt;- median(unlist(lapply(qux.md[ , classCols], sd))))

<span class="hljs-keyword">for</span> (i <span class="hljs-keyword">in</span> <span class="hljs-number">1</span>:nIter) {
  <span class="hljs-comment"># set up 1 respondent "trial"</span>
  pwsResp     &lt;- rnorm(nItems, mean=<span class="hljs-number">0</span>, sd=drawSD) <span class="hljs-comment"># "nItems" random normal, zero-centered simulated part worths</span>
  pwsResp     &lt;- scale(pwsResp, scale=<span class="hljs-literal">FALSE</span>) <span class="hljs-comment"># recenter to make sure they're zero-sum</span>

  <span class="hljs-comment"># iterate over nScreen tasks per respondent</span>
  drawMax &lt;- rep(<span class="hljs-literal">NA</span>, nScreens*<span class="hljs-number">2</span>)             <span class="hljs-comment"># hold the results across the screens for one respondent</span>
  <span class="hljs-keyword">for</span> (j <span class="hljs-keyword">in</span> <span class="hljs-number">1</span>:nScreens) {
    pwsDrawn    &lt;- sample(pwsResp, nShown)   <span class="hljs-comment"># get the part worths for 1 simulated task  </span>
    <span class="hljs-comment"># "best" choice</span>
    pwsExp      &lt;- exp(pwsDrawn)             <span class="hljs-comment"># exponentiate those part worths</span>
    pwsMaxShare &lt;- max(pwsExp) / sum(pwsExp) <span class="hljs-comment"># our best (most likely) prediction would be the max utility item</span>
    drawMax[j*<span class="hljs-number">2</span> - <span class="hljs-number">1</span>]  &lt;- pwsMaxShare
    <span class="hljs-comment"># repeat for the "worst" choice</span>
    pwsExp        &lt;- exp(-<span class="hljs-number">1</span> * pwsDrawn)      <span class="hljs-comment"># exponentiate part worths for the "worst" choice direction</span>
    pwsMaxShare   &lt;- max(pwsExp) / sum(pwsExp)   
    drawMax[j*<span class="hljs-number">2</span>]  &lt;- pwsMaxShare
  }
  <span class="hljs-comment"># calculate RLH from those observations</span>
  rlh         &lt;- exp(sum(log(drawMax[drawMax &gt; <span class="hljs-number">0</span>]), na.rm=<span class="hljs-literal">TRUE</span>) / length(drawMax))    <span class="hljs-comment"># safer equivalent of "prod(drawMax) ^ (1 / length(drawMax))"</span>
  rlhDraws[i] &lt;- rlh
  <span class="hljs-comment"># add a bit of error checking, just in case of an off by one error etc :-/</span>
  <span class="hljs-keyword">if</span> (length(drawMax) != nScreens * <span class="hljs-number">2</span>) <span class="hljs-keyword">warning</span>(<span class="hljs-string">"The vector of partworths is off somewhere!"</span>, i, <span class="hljs-string">":"</span>, drawmax)
}

<span class="hljs-comment"># density plot</span>
set.seed(<span class="hljs-number">98195</span>)    <span class="hljs-comment"># jittered points are slightly randomized</span>
p &lt;- ggplot(data=as.data.frame(rlhDraws), aes(x=rlhDraws, y=<span class="hljs-number">1</span>)) +
  geom_density_ridges(jittered_points=<span class="hljs-literal">TRUE</span>, 
                      alpha=<span class="hljs-number">0.1</span>, colour=<span class="hljs-string">"darkblue"</span>) +
  xlab(<span class="hljs-string">"Model fit score (RLH; &gt; 0.3 is good)"</span>) +
  ylab(<span class="hljs-string">"Relative prevalence (density)"</span>) +
  ggtitle(<span class="hljs-string">"RLH Simulation, Perfectly Made Choices (N=10000)"</span>) +
  theme_minimal()

p

<span class="hljs-comment"># where does our observed data fall in the distribution?</span>
ecdf(rlhDraws)(median(md.dat$RLH))               

<span class="hljs-comment"># combined density plot (using random subset of simulated data)</span>
compare.df &lt;- data.frame(RLH    = c(md.dat$RLH, rlhDraws),
                         Source = rep(c(<span class="hljs-string">"UXR"</span>, <span class="hljs-string">"Sim"</span>), 
                                      times=c(length(md.dat$RLH), 
                                              length(rlhDraws))))

<span class="hljs-comment"># density plot</span>
set.seed(<span class="hljs-number">98102</span>)    <span class="hljs-comment"># jittered points are slightly randomized</span>
p &lt;- ggplot(data=compare.df, aes(x=RLH, y=Source, color=Source)) +
  geom_density_ridges(jittered_points=<span class="hljs-literal">TRUE</span>, 
                      alpha=<span class="hljs-number">0.1</span>) +
  xlab(<span class="hljs-string">"RLH"</span>) +
  ylab(<span class="hljs-string">"Relative prevalence (density)"</span>) +
  ggtitle(<span class="hljs-string">"RLH Comparison: UXR Data vs. 'Perfect' Simulation"</span>) +
  theme_minimal()

p
</code></pre>
<hr />
<h3 id="heading-citations">Citations</h3>
<p>I’m reminding myself (and others) to systematically <strong>cite the important work others have done</strong> and that make R and other tools so valuable. Following are key citations for today’s code!</p>
<p>BTW, you can find citations for almost everything in R with the <code>citation()</code> command, such as <code>citation("ggridges")</code>.</p>
<ul>
<li><p>Orme, B (2019). <em>Consistency Cutoffs to Identify "Bad" Respondents in CBC, ACBC, and MaxDiff.</em> Sawtooth Software technical paper, available at <a target="_blank" href="https://content.sawtoothsoftware.com/assets/48af48f3-c01e-42ff-8447-6c8551a6d94f">https://content.sawtoothsoftware.com/assets/48af48f3-c01e-42ff-8447-6c8551a6d94f</a></p>
</li>
<li><p>R Core Team (2024). <em>R: A Language and Environment for Statistical Computing</em>. R Foundation for Statistical Computing, Vienna, Austria. <a target="_blank" href="https://www.R-project.org/">https://www.R-project.org/</a>. Version 4.4.1.</p>
</li>
<li><p>Schauberger P, Walker A (2024). <em>openxlsx</em>: Read, Write and Edit xlsx Files. R package version 4.2.6.1, <a target="_blank" href="https://CRAN.R-project.org/package=openxlsx">https://CRAN.R-project.org/package=openxlsx</a>.</p>
</li>
<li><p>Wickham, H (2016). <em>ggplot2</em>: Elegant Graphics for Data Analysis. Springer-Verlag New York.</p>
</li>
<li><p>Wilke C (2024). <em>ggridges</em>: Ridgeline Plots in 'ggplot2'. R package version 0.5.6, <a target="_blank" href="https://CRAN.R-project.org/package=ggridges">https://CRAN.R-project.org/package=ggridges</a>.</p>
</li>
</ul>
<p><a target="_blank" href="https://notbyai.fyi"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746999945541/9d224843-9e9f-44cc-98c5-276915794420.png" alt class="image--center mx-auto" /></a></p>
]]></content:encoded></item><item><title><![CDATA[Be a T-shaped Quant UXR: How Doing Qualitative Research Made Me a Better Quantitative UX Researcher]]></title><description><![CDATA[Hi there, my name is Kitty Xu, and I am a quantitative user experience researcher (a.k.a., Quant UXR). What is Quant UX, you ask? Over the years, I've published 3 articles on it, and you can read them here:

What is quantitative user experience resea...]]></description><link>https://quantuxblog.com/be-a-t-shaped-quant-uxr-how-doing-qualitative-research-made-me-a-better-quantitative-ux-researcher</link><guid isPermaLink="true">https://quantuxblog.com/be-a-t-shaped-quant-uxr-how-doing-qualitative-research-made-me-a-better-quantitative-ux-researcher</guid><category><![CDATA[quantux]]></category><category><![CDATA[#Ux research]]></category><category><![CDATA[career advice]]></category><dc:creator><![CDATA[Kitty Xu]]></dc:creator><pubDate>Tue, 07 Jan 2025 16:11:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735703320479/7537bea5-17d5-48b0-9179-faa31f2556e2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi there, my name is Kitty Xu, and I am a quantitative user experience researcher (a.k.a., Quant UXR). What is Quant UX, you ask? Over the years, I've published 3 articles on it, and you can read them here:</p>
<ol>
<li><p><a target="_blank" href="https://medium.com/pinterest-studio/what-is-quantitative-user-experience-research-at-pinterest-8eb17c69a0fc">What is quantitative user experience research at Pinterest?</a></p>
</li>
<li><p><a target="_blank" href="https://medium.com/pinterest-studio/youre-a-stem-major-your-dream-job-might-be-in-design-b3f947941360">You’re a STEM major? Your dream job might be in Design</a></p>
</li>
<li><p><a target="_blank" href="https://medium.com/pinterest-studio/from-brain-science-to-quantitative-ux-531eeec82806">From brain science to quantitative UX</a></p>
</li>
</ol>
<p>Today, I want to tell a different story, one that involves doing <em>qualitative</em> UX research (a.k.a., qual UX) and how it made me a better Quant UXR.</p>
<p>For clarification, I’m not talking about partnering with a qual UXR. That’s often beneficial, especially if you have the privilege of working with multiple researchers (<a target="_blank" href="https://medium.com/pinterest-studio/from-brain-science-to-quantitative-ux-531eeec82806">Lesson #4 here</a>)! What I'm additionally arguing is that <strong>Quant UXRs can develop and execute qual UX research, and this will reciprocally improve their Quant UX research capabilities.</strong></p>
<p>If you consider yourself someone who always defaults to quant research methods and you are skeptical about whether building up some qual research experience in your portfolio will actually make you a better Quant UXR — or if you're just too intimidated to execute research outside your methodological comfort zone — read on ...</p>
<h2 id="heading-the-limitation-of-technical-expertise"><strong>The limitation of technical expertise</strong></h2>
<p>In the mid-2010s, <a target="_blank" href="https://www.linkedin.com/pulse/behind-scenes-making-first-ever-quant-uxr-conference-june-2022/">Quant UX research was still defining its professional identity</a>. When I entered the field, I thought what differentiated Quant UXRs from their qual counterparts was <a target="_blank" href="https://quantuxblog.com/skills-combination-for-quant-ux-applications">technical expertise</a> — particularly the ability to design surveys, query log data, and program in languages like R or Python to format, manipulate, and analyze large datasets, and to run statistical modeling and create data visualizations. Back then, I believed that advancing these technical capabilities was the key to becoming a better Quant UXR.</p>
<p>Early in my career, my work reflected this mindset. I wanted to be an "numbers expert," showcasing advanced statistical models, intricate SQL queries, and meticulously crafted surveys. I built strength in transforming survey responses and behavioral log data into actionable user insights at scale.</p>
<p>However, <strong>technical mastery alone doesn't guarantee research impact.</strong> As my career progressed, I faced increasingly complex and ambiguous challenges that couldn't be solved with complex data analysis alone. Such research required a deep understanding of nuanced spaces and the ability to identify opportunities for meaningful product improvement, balancing both user experience and business needs.</p>
<p>At the same time, I came to appreciate a hierarchy of research excellence:</p>
<ul>
<li><p>A <em>good</em> researcher <strong>answers a question effectively</strong>.</p>
</li>
<li><p>A <em>great</em> researcher <strong>selects the most appropriate tools</strong> to answer a question effectively.</p>
</li>
<li><p>An <em>amazing</em> researcher <strong>ensures they are tackling the right question</strong>, then selects the most appropriate tools to answer it.</p>
</li>
<li><p>An <em>exceptional</em> researcher ensures they are tackling the right question, then selects the most appropriate tools to answer it, <strong>and crafts compelling narratives that inspire action.</strong></p>
</li>
</ul>
<p><em>Modifiers used to describe a researcher shall not be used for a survey scale question</em> :)<em>.</em></p>
<p>My work evolved from technical execution to uncovering deeper user insights that could shape business strategy. Technical skills remained critical, but they became tools in service of a more fundamental goal: understanding user experience at its deepest, most nuanced level.</p>
<p><strong>The true measure of research impact, I realized, isn't the complexity of the analysis — it's the potential for meaningful change in both user experience and business outcomes.</strong></p>
<h2 id="heading-first-steps-into-qualitative-research"><strong>First steps into qualitative research</strong></h2>
<blockquote>
<p>“A fundamental principle of innovation or creative thinking is to start with empathy.” – IDEO</p>
</blockquote>
<p>People <a target="_blank" href="https://www.ideo.com/journal/build-your-creative-confidence-empathy-maps">SAY, DO, THINK and FEEL</a>, and these don’t always align. <strong>Quant UX research focuses on understanding <em>what</em> (and <em>how much)</em> people DO and SAY they feel about a product or experience at scale by collecting and analyzing empirical evidence. In contrast, qual UX research delves deeper, exploring <em>why</em> people THINK and FEEL the way they do, uncovering nuanced motivations and underlying emotions.</strong> Combining them gives me a richer data set to analyze patterns, synthesize the meaning, identify opportunities, and build a compelling narrative.</p>
<p>My initial experience with qual research came through collaborations with skilled qual UXRs. This was done in two ways: (a) observing and leveraging the work of my qual UXR colleagues, and (b) designing and commissioning qual studies done by external research vendors. Through projects ranging from consumer shopping journeys and advertiser trust to teen safety and user segmentation, I developed a deeper appreciation for the nuanced and layered insights that qual research provides, extending far beyond what my data charts and dashboards alone can reveal.</p>
<p>These early collaborations were eye-opening. While I wasn't conducting the user interviews directly, I learned to:</p>
<ul>
<li><p>Leverage qual findings to complement and enhance my quant analysis.</p>
</li>
<li><p>Triangulate quant and qual insights to craft narratives that are both data-driven and human-centered, combining breadth with depth.</p>
</li>
<li><p>Recognize and value contextual details from the periphery of observations and interviews, uncovering hidden insights that often prove invaluable during research synthesis.</p>
</li>
</ul>
<p>I thought I had gained a solid understanding of qual research through these collaborations, but conducting my own qual research projects taught me far more. Over the past couple years, I led several international research initiatives, employing both remote and in-person methodologies. The remote studies consisted of in-depth interviews (IDIs) conducted over video calls, working across multiple time zones and languages with the help of simultaneous interpreters.</p>
<p>For field research, I sourced and partnered with local research agency partners to operationalize the study, and facilitate on-site market deep-dives. I brought cross-functional teammates across the world to meet and interact with users in their own local context. My travel-mates spanned exec leadership, product managers (PMs), engineers, designers, analysts, and marketers. These studies were conducted across China, South Korea and Turkey.</p>
<p>For a Quant UXR who revels in the structure and predictability of quant methods, <strong>the unstructured nature of qual research initially felt messy, disorienting and deeply uncomfortable. Yet, this discomfort was a catalyst that allowed me to grow</strong>.</p>
<p>I was fortunate to learn from veteran qual UXRs who provided invaluable mentorship throughout my growth. They shared essential resources like recruiting screeners and interview protocols, which gave me a strong foundation to build upon. By shadowing their interview sessions, I gained firsthand experience with effective interviewing techniques and learned to handle unexpected challenges, from technical issues to participant no-shows. Their guidance on working with international research vendors proved especially valuable, helping me navigate cross-cultural research complexities.</p>
<p>With their support and encouragement, I developed confidence in leading end-to-end research processes, developing skills in:</p>
<ul>
<li><p><strong>Research design and methodology:</strong> Crafting nuanced interview protocols and designing diverse research methodologies, including IDIs, focus groups, expert interviews, co-creation workshops, and dinner mixers that facilitate authentic conversations between my cross-functional teammates and research participants (who are English language learners in our target markets).</p>
</li>
<li><p><strong>Global research operations:</strong> Sourcing, vetting, and relationship managing of local qual UX agencies and facilities in target markets.</p>
</li>
<li><p><strong>Participant recruitment:</strong> Developing and localizing screeners to recruit research participants across complex cultural landscapes, languages, and timezones. (This one was particularly hard for me, not going to sugar coat it.)</p>
</li>
<li><p><strong>Moderation and facilitation:</strong> Conducting IDIs through simultaneous interpreters across multiple languages. In one challenging case, I conducted an interview in both English and Mandarin simultaneously to accommodate both the participant and stakeholders observing the session.</p>
</li>
<li><p><strong>Qualitative data analysis and synthesis:</strong> Synthesizing qual data, including individual stories, observations, and unstructured data, into a cohesive narrative.</p>
</li>
<li><p><strong>Leadership in cross-functional collaboration:</strong> Guiding large cross-functional teams during intensive international research trips, and facilitating cross-functional team debriefs and synthesis sessions.</p>
</li>
</ul>
<p>These projects offered far more than just data collection. <strong>By conducting qual research myself, I learned to step back, to shift my focus from being the primary driver of the research to truly listen — not just to what the participants said, but also what they didn't say, and the nuances between the lines.</strong></p>
<p>This shift in perspective taught me to set aside my own preconceptions and embrace genuine curiosity about what I might discover through these intimate exchanges across languages and cultures. The resulting data, synthesized insights, and the stories we crafted felt more human and more personally meaningful, because they were grounded in real human experiences.</p>
<h2 id="heading-the-power-of-qualitative-research-for-quant-uxrs"><strong>The power of qualitative research for Quant UXRs</strong></h2>
<p>Qual research has been instrumental in elevating my work as a Quant UXR. By delving into the nuances of user experience, it has enabled me to reframe research questions, develop holistic research strategies, and channel user empathy for greater outcomes, empowers me to deliver more impactful insights, influencing stakeholders to prioritize the most critical opportunities with conviction.</p>
<h3 id="heading-reframing-research-questions"><strong>Reframing research questions</strong></h3>
<p>In quant research, I often started with preset questions. However, qual research taught me the importance of understanding the user's world first. <strong>By listening to users' unfiltered experiences and identifying pain points they might not articulate in a survey, I am able to challenge assumptions and uncover deeper issues.</strong></p>
<p>For example, when I worked on an educational product that connects English language learners with native English-speaking tutors through 1-on-1 online lessons, one data analysis revealed that despite offering free trial lessons to all new users, free trial lesson usage was low. The growth product team initially focused on optimizing the free trial's visibility on the homepage. However during an international research trip, we observed new users struggling to understand how to use the product. This led us to reframe the problem from "why aren't new users using free trial lessons?" to "how can we improve new user comprehension during onboarding?" The team shifted its focus to educating new users on how to navigate the product, and take key actions earlier in their learning journey, which resulted in a more significant impact on free trial lesson adoption, and ultimately increased the conversion rate.</p>
<h3 id="heading-developing-holistic-research-strategies"><strong>Developing holistic research strategies</strong></h3>
<p>As a Quant UXR leading qual studies, it allowed me to strategically design research that investigated the "why" behind my statistical findings. <strong>Rather than participating in other qual UXRs’ work and following someone else's research protocol, I could craft interview questions that directly probed into patterns I'd identified in my data, creating a powerful feedback loop between methods.</strong></p>
<p>For example, while my quant research report suggested that English language learners highly value access to native English-speaking tutors, the product team, based in Silicon Valley, initially prioritized AI technology in our new user acquisition strategy. When I took the team on an international research trip and immersed ourselves in non-English-speaking contexts, the team realized that many learners faced significant barriers to accessing native speakers, far greater than the team had initially imagined. One learner poignantly expressed, "Before discovering your product, learning English meant waiting 45 minutes in a classroom for a chance to speak one sentence with my English teacher."</p>
<p>Armed with both the statistical evidence from the quant report and these powerful firsthand experiences, my team was compelled to reevaluate our strategy and re-emphasize the value of native speaker access in our user acquisition efforts.</p>
<h3 id="heading-channeling-user-empathy-for-greater-outcomes"><strong>Channeling user empathy for greater outcomes</strong></h3>
<p>Perhaps, most significantly, qualitative research is pivotal in cultivating a shared, profound empathy for users among myself and stakeholders. <strong>By directly engaging with the people we design for, we collectively gain a deeper understanding of their motivations and frustrations — and the often unspoken anxieties that drive their behavior.</strong></p>
<p>For example, in my survey study, we discovered that many English language learners in the APAC region highly valued the curriculum during lessons with native English-speaking tutors. Initially, our takeaway was that APAC learners have a tendency to prefer structured lessons over free-form speaking. However, after conducting interviews with learners, we uncovered a deeper insight: structured lessons with a pre-selected curriculum served as an avoidance mechanism for these learners’ anxiety, alleviating the dreaded fear of running out of topics to discuss with their tutor — a fear that significantly impacted their learning experience! This insight helped our product team not only build more experiences that allowed structured English lessons, but also explore other ways they could address the more impactful pain point and opportunity — helping reduce learner anxiety.</p>
<h2 id="heading-tips-for-getting-started-with-qual-work"><strong>Tips for getting started with qual work</strong></h2>
<p>For Quant UXRs hesitant to dive into qual work, if you’ve read this far, I hope you are feeling more ready to take on the new challenge! Here are a few tips to get you started:</p>
<ul>
<li><p><strong>Get exposure</strong></p>
<ul>
<li><p>Start small: observe qualitative interviews</p>
</li>
<li><p>Find at least one qual researcher as your mentor (I got really lucky on this one!)</p>
</li>
</ul>
</li>
<li><p><strong>Change mindset/attitude</strong></p>
<ul>
<li><p>Approach individuals with curiosity and an open mind, recognizing that even small samples can offer valuable insights and unexpected discoveries</p>
</li>
<li><p>View it as diversifying the tools in your toolkit with new ones, not replacing them</p>
</li>
</ul>
</li>
<li><p><strong>Take action</strong></p>
<ul>
<li><p>Practice active listening</p>
</li>
<li><p>Ask open-ended questions</p>
</li>
<li><p>Anytime you write a survey, before going live, pre-test it. Do this with a few users or coworkers live (even friends or family!), using a <a target="_blank" href="https://www.lyssna.com/guides/think-aloud-protocol/">think aloud protocol</a></p>
</li>
<li><p>Ask for opportunities to conduct lightweight user interviews (for more references on how to conduct user interviews, <a target="_blank" href="https://portigal.com/Books/interviewing-users-2/">Interviewing Users: How to Uncover Compelling Insights (2nd Edition)</a>, by Steve Portigal, is a great book)</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-ux-managers"><strong>UX managers?</strong></h2>
<p>Give your Quant UXRs room for qualitative work! It’s not something they do as a <em>distraction</em> from Quant UX research. Rather, it is a <em>core part</em> of great research, regardless of role.</p>
<h2 id="heading-the-evolving-research-identity"><strong>The evolving research identity</strong></h2>
<p>My research identity is no longer about being "quant" or "qual”, because these are tools. <strong>It's about being a</strong> <a target="_blank" href="https://chiefexecutive.net/ideo-ceo-tim-brown-t-shaped-stars-the-backbone-of-ideoaes-collaborative-culture__trashed/"><strong>T-shaped</strong></a><strong>,</strong> <a target="_blank" href="https://mitsloan.mit.edu/ideas-made-to-matter/3-traits-entrepreneurial-mindset"><strong>entrepreneurial-minded</strong></a> <strong>UXR</strong> who specializes in quant research methods — the vertical stroke of the “T”, the depth of my quant tools – but also is experienced in crafting and executing qual research – the horizontal stroke of the “T”, the breath of my research toolkit, AND is willing to do whatever it takes to get the insight needed to understand complex users and businesses, and explore opportunities at their intersection.</p>
<p><em>This essay was inspired by the many many long conversations I had with Gabe Trinofi, Altay Sendil, Cassandra Rowe, Chris Chapman, Scott Tong and Omar Seyal, and is dedicated to all the qualitative UXRs whose work inspired me to be a better researcher.</em></p>
<p><em>Special shout-out to Altay Sendil for his meticulous edits, and Jeff Miao for his creative illustration!</em></p>
]]></content:encoded></item><item><title><![CDATA[Individual Scores in Choice Models, Part 3: Respondent Segments]]></title><description><![CDATA[This is Part 3 of a series examining how to work in R with individual-level estimates for preferences. We’re using real data from a MaxDiff survey on the preferences of N=308 respondents interested to take classes from the Quant UX Association.
If yo...]]></description><link>https://quantuxblog.com/individual-scores-in-choice-models-part-3-respondent-segments</link><guid isPermaLink="true">https://quantuxblog.com/individual-scores-in-choice-models-part-3-respondent-segments</guid><category><![CDATA[mrx]]></category><category><![CDATA[R Language]]></category><category><![CDATA[quantux]]></category><category><![CDATA[#Ux research]]></category><category><![CDATA[surveys]]></category><category><![CDATA[maxdiff]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Thu, 07 Nov 2024 16:18:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/i3EjqaHIyO0/upload/4a094ff832eee14613825fa0b54e20de.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is <strong>Part 3 of a series</strong> examining how to work in R with individual-level estimates for preferences. We’re using real data from a <a target="_blank" href="https://quantuxblog.com/easy-maxdiff-in-r">MaxDiff survey</a> on the preferences of N=308 respondents interested to take <a target="_blank" href="https://www.quantuxcon.org/classes">classes</a> from the Quant UX Association.</p>
<p>If you haven’t read Posts 1 and 2, you’ll want to catch up on the background and <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-1-data-averages">basic data in Post 1</a>, and on the question of correlational patterns of interest in <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-2-correlations-among-items">Post 2</a>. As usual, I illustrate everything with R code and will discuss the code as we go, with all of the code compiled at the end.</p>
<p>The topic of this Part 3 is this: are there <strong>clusters (segments)</strong> of <em>respondents</em> who have similar patterns in their interest in classes? As my coauthors and I discuss in the <a target="_blank" href="https://www.amazon.com/Marketing-Research-Analytics-Use/dp/3030143155">R book</a> and the <a target="_blank" href="https://www.amazon.com/Python-Marketing-Research-Analytics-Schwarz/dp/3030497194">Python book</a>, the process of finding segments in real data is complex and there is no magic answer. Segmentation is an iterative process of finding some <strong>number of segments</strong> and seeing whether those segments are <strong>useful and interpretable</strong> for your business need.</p>
<p>In this post, I discuss <strong>one way</strong> to find the number of segments and to visualize the results … with a big caveat: there is much more to say! This is <em>one</em> <em>illustration</em> and is <em>not a prescription</em>. No blog post can give a complete recipe for clustering. For comprehensive discussion and learning, see “Learning More” at the end of this post. Also stay tuned for a future “Segmentation FAQ” blog post soon!</p>
<p><strong>Warning: long post</strong>! I like talking about segmentation (both pro and con) :) Like many posts in this blog, this is intended to complement other writings. It complements the <a target="_blank" href="https://www.amazon.com/Marketing-Research-Analytics-Use/dp/3030143155">R book</a> and <a target="_blank" href="https://www.amazon.com/Python-Marketing-Research-Analytics-Schwarz/dp/3030497194">Python book</a> for conjoint, and the <a target="_blank" href="https://www.amazon.com/Quantitative-User-Experience-Research-Understanding/dp/1484292677">Quant UX book</a> for MaxDiff.</p>
<hr />
<h3 id="heading-first-get-the-data">First, Get the Data</h3>
<p>You can read <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-1-data-averages">Posts 1</a> and <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-2-correlations-among-items">2</a> for background. But if you’re jumping in here, the following R code will load the data. These are N=308 responses estimating researchers’ levels of interest in Quant UX classes, from a MaxDiff survey.</p>
<p>You can get the data with:</p>
<pre><code class="lang-r"><span class="hljs-comment"># get the data; repeating here for blog post 3, see post 1 for details</span>
<span class="hljs-keyword">library</span>(openxlsx) <span class="hljs-comment"># install if needed</span>
md.dat &lt;- read.xlsx(<span class="hljs-string">"https://quantuxbook.com/misc/QUX%20Survey%202024%20-%20Future%20Classes%20-%20MaxDiff%20Individual%20raw%20scores.xlsx"</span>)   <span class="hljs-comment"># </span>
md.dat$Anchor &lt;- <span class="hljs-literal">NULL</span>
names(md.dat)[<span class="hljs-number">3</span>:<span class="hljs-number">16</span>] &lt;- c(<span class="hljs-string">"Choice Models"</span>,  <span class="hljs-string">"Surveys"</span>,       <span class="hljs-string">"Log Sequences"</span>,    <span class="hljs-string">"Psychometrics"</span>, 
                         <span class="hljs-string">"R Programming"</span>,  <span class="hljs-string">"Pricing"</span>,       <span class="hljs-string">"UX Metrics"</span>,       <span class="hljs-string">"Bayes Stats"</span>,
                         <span class="hljs-string">"Text Analytics"</span>, <span class="hljs-string">"Causal Models"</span>, <span class="hljs-string">"Interviewer-ing"</span>,  <span class="hljs-string">"Advanced Choice"</span>, 
                         <span class="hljs-string">"Segmentation"</span>,   <span class="hljs-string">"Metrics Sprints"</span>)
classCols &lt;- <span class="hljs-number">3</span>:ncol(md.dat)    <span class="hljs-comment"># generally, Sawtooth exported utilities start in column 3</span>
</code></pre>
<p>I recommend to follow along live in R, installing packages as needed. All the R code is given as I go, and also compiled at the end of the post.</p>
<hr />
<h3 id="heading-background-general-clustering-vs-latent-class-analysis">Background: General Clustering vs Latent Class Analysis</h3>
<p>At a high level, there are two approaches to clustering/segmenting data from choice surveys.</p>
<p>One approach is to estimate respondents' preferences first as a whole sample, and then apply <strong>general clustering</strong> methods <em>later</em> to those estimates to find segments. My experience suggests that a majority of analysts use this approach for several reasons. First, it is easiest because it divides analysis into tidy stages. Second, you can use the widest range of tools and methods. Third, it does not require specialized software for the clustering portion, but uses general methods from R, Python, or wherever. Fourth, the approach can be used with any kind of data and with mixed types of data.</p>
<p>The second approach is to model segments <em>at the same time</em> that you estimate individuals’ preferences. This is the approach taken by <strong>latent class analysis</strong> (LCA), as applied to choice surveys. The primary advantage of LCA is that, by doing both steps together (estimation + segmentation), it is more robust to sparse data and noise in the data. On the other hand, LCA requires more specific assumptions about the data, along with specialized tools.</p>
<p>In the choice modeling community, the most common tools used for choice model LCA are Statistical Innovation’s <em>Latent Gold</em>, Sawtooth Software’s <em>Lighthouse Studio</em>, and proprietary code bases within analytics firms. These may add cost and complexity to analyses, while limiting the generality of what you can do. By contrast, general clustering methods are widely available in R and other platforms. (Note that choice model LCA is possible in <strong>R</strong> … but the effort is beyond reasonable levels for general practitioners. FWIW, I personally <em>avoid</em> doing choice model LCA in R.)</p>
<p><strong>In this article, I demonstrate use the general clustering approach</strong> … with one note: if you <em>frequently</em> need to cluster <em>choice model</em> data, you should check out Sawtooth Software’s Lighthouse Studio and/or Statistical Innovations’s Latent Gold. Their tailored functionality can be helpful, efficient, and more precise for practitioners. For my part, I often use LCA in Sawtooth’s Lighthouse Studio. However, I <em>also</em> often use general clustering as this post illustrates.</p>
<p>In short, although I use choice survey data to demonstrate the methods here, the methods here are general. You can apply the tools and code in this post to a wide variety of data sources.</p>
<hr />
<h3 id="heading-finding-a-number-of-segments-part-1">Finding a Number of Segments: Part 1</h3>
<p><strong>The first question in segmentation is this: are there segments at all?</strong> And if so, how many?</p>
<p>A <em>reasonable assumption</em> — especially in high dimensional space, i.e., in data with many variables — <em>is that people do</em> not <em>cluster together</em>, and that instead, they differ from one another in unstructured (“random”) ways. Some jargon for that is that the data exhibit a <em>multivariate normal distribution (MVN) with one component</em>. In other words, there is a single “segment” and every respondent is a member!</p>
<p>My take — see the “clustering” chapters in the <a target="_blank" href="https://www.amazon.com/Marketing-Research-Analytics-Use/dp/3030143155">R book</a> and the <a target="_blank" href="https://www.amazon.com/Python-Marketing-Research-Analytics-Schwarz/dp/3030497194">Python book</a> — is that MVN data with 1 cluster is quite often the “best” statistical answer … but the <strong>number of segments is not primarily a statistical question</strong>. There are hundreds of methods to cluster data and they typically give different answers. The question is whether we can find segments that are <em>useful and informative</em> for a business question, while secondarily optimizing statistically.</p>
<p>There are several ways we might inspect the data to see whether it appears to have clusters. One way is a <strong>model-based</strong> approach to determines how many clusters may underlie the data. The <code>mclust</code> library is one of my favorites. In R we can fit a clustering (segmentation) model to the MaxDiff data as follows:</p>
<pre><code class="lang-r"><span class="hljs-comment"># mclust uses model-based fit estimates to determine best number of latent clusters</span>
<span class="hljs-keyword">library</span>(mclust)    <span class="hljs-comment"># install if needed</span>
md.mc &lt;- Mclust(md.dat[ , classCols])
summary(md.mc)
</code></pre>
<p>The result is:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729526558978/24be6dc9-1825-4ccf-8bd4-cee8baea4703.png" alt="results of the mclust summary as produced by the code, with 1 cluster" class="image--center mx-auto" /></p>
<p>It found a multivariate normal distribution with one component. In other words, the best fit is <strong>1 segment</strong>.</p>
<p>Another approach is <strong>visual</strong>. We could use a dimensional reduction approach to represent the data in a visually suitable number of dimensions — 2 or 3 dimensions — and then inspect it to see whether there is some obvious clustering.</p>
<p>There are many dimensional reduction models but a modern starting point is UMAP (<em>uniform manifold approximation and projection</em>). UMAP attempts to “project” (i.e., re-estimate) data in a lower number of nonlinear dimensions (“uniform manifold”) while also preserving (“approximating”) the structure of the complete data. (For an introduction, see Coenen and Pearce on <a target="_blank" href="https://pair-code.github.io/understanding-umap/">“Understanding UMAP” here</a>.)</p>
<p>We can visualize the MaxDiff class interest data with UMAP as follows. First we fit a UMAP projection to the data, setting a random seed to make it reproducible:</p>
<pre><code class="lang-r"><span class="hljs-keyword">library</span>(umap)
<span class="hljs-comment"># get umap representation of the individual interest data</span>
umap.config &lt;- umap.defaults
umap.config$random_state &lt;- <span class="hljs-number">98101</span>

umap.sol &lt;- umap(md.dat[ , classCols], config = umap.config)
umap.dat &lt;- data.frame(umap.sol$layout)
<span class="hljs-comment"># review the kind of data we have</span>
str(umap.dat)
</code></pre>
<p>Using <code>str()</code> we see that the data now has 308 observations reduced to 2 dimensions, <code>X1</code> and <code>X2</code>. The particular values don’t really matter in themselves; it is the <em>relationships</em> among the values that reproduce the relative structure of the observations vs. one another:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729633063693/8967ace8-be13-4e47-9b42-03817be3f101.png" alt="data from UMAP, a data frame with values in 2 dimensions (columns)" class="image--center mx-auto" /></p>
<p>Now we can plot the UMAP points just like any other X / Y data using a simple scatterplot of the points:</p>
<pre><code class="lang-r"><span class="hljs-comment"># plot it</span>
ggplot(umap.dat, aes(x = X1, y = X2)) +
  geom_point() +
  xlab(<span class="hljs-string">"High order dimension 1"</span>) + ylab(<span class="hljs-string">"High order dimension 2"</span>) +
  theme_minimal()
</code></pre>
<p>Here’s the chart:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729633199019/380c3364-2c31-425c-b940-132efe12388d.png" alt="an ellipsoidal plot of the points that is very smoothly round with no obvious clustering from initial visual inspection" class="image--center mx-auto" /></p>
<p><strong>This chart shows no obvious “clusters” of respondents</strong>. It is slightly denser in some areas than others but overall has an appearance of being smoothly round without separation among groups. This appearance is not a statistical test, but it is congruent with the idea that there are not multiple obvious segments but 1 segment of everyone.</p>
<p>So far we are striking out on finding segments. The data set strongly appears to come from a single multivariate normal population (i.e., from one segment).</p>
<p>But remember I mentioned that there are hundreds of clustering methods? Let’s keep prodding and see if we can find something else that is useful!</p>
<hr />
<h3 id="heading-finding-a-number-of-segments-part-2">Finding a Number of Segments: Part 2</h3>
<p>Another R library that suggests the number of clusters in data is <code>NbClust</code>. It implements 8 clustering methods with a choice of 6 different distance (dissimilarity) metrics applicable to each one. After you select a distance metric and clustering method, it reports 30 heuristics (sometimes fewer, for reasons I’ll skip) that each suggest the number of clusters.</p>
<p>That’s easier to understand from an example:</p>
<pre><code class="lang-r"><span class="hljs-keyword">library</span>(NbClust)
md.nbc &lt;- NbClust(md.dat[ , classCols],
                  min.nc = <span class="hljs-number">2</span>, max.nc = <span class="hljs-number">10</span>,
                  method = <span class="hljs-string">"ward.D"</span>, 
                  distance = <span class="hljs-string">"euclidean"</span>)
</code></pre>
<p>In this code, I ask it to consider anywhere from 2 to 10 clusters (<code>min.nc</code>, <code>max.nc</code>). For a distance metric, I use the classic Euclidean metric (i.e., triangular hypotenuse, except extended to many dimensions), and cluster them with the “Ward’s D” method (see <code>?NbClust</code> for details).</p>
<p>Here are the results, first for 24 methods that give textual results, and then for 2 methods that give graphical results:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729634680960/67d11ca9-89cf-4cab-95a2-e483f480de41.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729634713712/d59b50ba-6c9c-478c-be80-63dd6cd79a43.png" alt class="image--center mx-auto" /></p>
<p>The textual results say that a “winner takes all” vote by the methods indicates <strong>2 clusters</strong> (as chosen by 9 out of 24 methods). However, almost as many methods (7 out of 24) suggest <strong>5 clusters</strong>. In the graphical results, both charts suggest 2 clusters as the best fit; and one of them suggests 5 clusters as a secondary possibility.</p>
<p>So, should we choose 2 clusters? In practice, I believe <strong>it is rarely useful or desired (by stakeholders) to discuss only 2 clusters</strong> — unless we are strictly speaking of likelihood of a binary outcome (such as purchase behavior). For purposes of <em>understanding</em> customers and users, binary options are rarely illuminating.</p>
<p>Thus, instead of 2 clusters, let’s look at the “second best” option of 5 clusters. What would that solution tell us?</p>
<p>To begin with, we’ll impose a 5 cluster solution. There are many ways to do that, but in this case we can simply run <code>NbClust()</code> again, setting the minimum number to 5:</p>
<pre><code class="lang-r"><span class="hljs-comment"># we'll try a 5 cluster solution instead of 2</span>
md.nbc &lt;- NbClust(md.dat[ , classCols],
                  min.nc = <span class="hljs-number">5</span>, max.nc = <span class="hljs-number">10</span>,
                  method = <span class="hljs-string">"ward.D"</span>, 
                  distance = <span class="hljs-string">"euclidean"</span>)

md.segs &lt;- md.nbc$Best.partition
table(md.segs)
proportions(table(md.segs))
</code></pre>
<p>This code runs <code>NbClust</code> again, with a minimum solution of 5 clusters (which is then found to be the best option with that limitation). We extract the segment assignment for each respondents (<code>$Best.partition</code>) and save them as <code>md.segs</code>. The final two lines show the counts of segment assignments and their relative proportions:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729641005386/8c784c8c-181e-4656-8d67-3d6911d29b1c.png" alt class="image--center mx-auto" /></p>
<p>The smallest segment (segment 5) has 10% of respondents, while the largest (segment 1) has 28% of respondents. That is a reasonable spread. It does not suffer the common problem where one or two segments have a very small proportion of respondents such as only 2% or 5% of respondents.</p>
<p>Next we’ll see whether the segments differ in <em>interpretable</em> ways.</p>
<hr />
<h3 id="heading-visualizing-segments-in-dimensional-space">Visualizing Segments in Dimensional Space</h3>
<p>Remember the UMAP chart above? Now that we have segment assignments, we can add those to the chart. Are there visual patterns when the segments are plotted in the higher order dimensions found by UMAP?</p>
<p>Here’s the code. It’s the same as above, except it now colors the points according to which segment each respondent is in (<code>md.segs</code>):</p>
<pre><code class="lang-r"><span class="hljs-comment"># add the 5-segment membership to the UMAP data</span>
umap.dat$Segment &lt;- factor(paste0(<span class="hljs-string">"S"</span>, md.segs))

<span class="hljs-comment"># plot it with segments</span>
ggplot(umap.dat, aes(x = X1, y = X2, colour = Segment)) +
  geom_point() +
  xlab(<span class="hljs-string">"High order dimension 1"</span>) + ylab(<span class="hljs-string">"High order dimension 2"</span>) +
  theme_minimal() +
  ggtitle(<span class="hljs-string">"Dimensional map with 5 segments"</span>)
</code></pre>
<p>Here’s the result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729641289278/029031e1-6bba-44c2-b7b9-0789b2b1ab18.png" alt="the same UMAP chart as above, but the respondents are colored by segment assignment. There are 5 clear slices of color." class="image--center mx-auto" /></p>
<p><strong>Whoa</strong>! This is a chart to make a Quant UXer very happy! In it, there are five extremely clear slices of the plot. Almost all of the members of each segment are close to others in the same segment — which is to say that the segments map very cleanly to the higher order dimensions of the data. That suggests that we may see real differences when we examine those dimensions (interest in different classes).</p>
<p>But first, I hope you’re thinking to yourself, “<em>Wait, am I fooling myself</em>? Maybe the colors being close together doesn’t mean much.” Good question!</p>
<p>Let’s double-check ourselves by comparing what the UMAP chart would look like if the segments were <em>randomly</em> assigned. To do that, we shuffle the same set of assigned segment memberships (using <code>sample()</code>) such that they are randomly assigned to different rows of the UMAP data, and plot them again:</p>
<pre><code class="lang-r"><span class="hljs-comment"># make sure we're not fooling ourselves ...</span>
<span class="hljs-comment"># what would it look like if the segments were random?</span>
set.seed(<span class="hljs-number">98107</span>)
<span class="hljs-comment"># the same segments but in random order</span>
umap.dat$RndSeg &lt;- sample(umap.dat$Segment)
<span class="hljs-comment"># plot those</span>
ggplot(umap.dat, aes(x = X1, y = X2, colour = RndSeg)) +
  geom_point() +
  xlab(<span class="hljs-string">"High order dimension 1"</span>) + ylab(<span class="hljs-string">"High order dimension 2"</span>) +
  theme_minimal()+
  ggtitle(<span class="hljs-string">"Dimensional map with RANDOM segments"</span>)
</code></pre>
<p>The result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729641899282/ba88dc56-c552-4ca6-b4c1-b6f08e03be5d.png" alt="same chart as above, except the 5 segments have been assigned randomly as a permutation of the &quot;real&quot; segment assignments. Now the chart shows a random mix of colors with no segment structure." class="image--center mx-auto" /></p>
<p>In this chart, there is no pattern to the colors for segment assignment. This tells us that the extremely clear pattern shown in the previous chart reflects real structure of the segments, relative to the data; the appearance was not simply an accident.</p>
<p><strong>In short, the five segments align strongly with real properties of the data</strong>. Now the question becomes: <em>is that alignment interpretable and useful</em>? I look at that question next.</p>
<hr />
<h3 id="heading-visualize-segments-with-a-heat-map">Visualize Segments with a Heat Map</h3>
<p>In the above analyses, we found — or, if you prefer, “imposed” — segment assignments that show statistical differentiation (the assessment heuristics in <code>NbClust</code>) and that align with the high dimensional data (the segment visual differences in UMAP). But are those results merely statistical or are they strong enough to be <em>meaningful</em> as a practical matter?</p>
<p>To find out, I’ll plot a <strong>heat map</strong> for class interest, broken out by segment. And, as you’ve probably guessed from my previous posts, I’ll make it a <strong>function</strong> :) With nothing more than minor tweaks (at most), this function will work for segmentation cross-tabs from many different kinds of data sets.</p>
<p>Here’s the function, with comments following below:</p>
<pre><code class="lang-r">seg.heat &lt;- <span class="hljs-keyword">function</span>(dat, segs) {
  <span class="hljs-keyword">library</span>(superheat)   <span class="hljs-comment"># install if needed</span>
  <span class="hljs-comment"># aggregate mean value by segment</span>
  heat.sum &lt;- data.frame(t(aggregate(. ~ segs, data = dat, mean))[-<span class="hljs-number">1</span>, ])
  <span class="hljs-comment"># make the column names reflect the segment number and size (%)</span>
  names(heat.sum) &lt;- paste0(<span class="hljs-string">"S"</span>, <span class="hljs-number">1</span>:max(segs), <span class="hljs-string">" ("</span>, 
                            round(prop.table(table(segs)), <span class="hljs-number">2</span>)*<span class="hljs-number">100</span>, <span class="hljs-string">"%)"</span>)
  <span class="hljs-comment"># draw it with superheat</span>
  superheat(heat.sum, 
            heat.pal = c(<span class="hljs-string">"red3"</span>, <span class="hljs-string">"white"</span>, <span class="hljs-string">"green3"</span>),
            grid.hline.col = <span class="hljs-string">"white"</span>, grid.vline.col = <span class="hljs-string">"white"</span>,
            pretty.order.rows = <span class="hljs-literal">TRUE</span>,
            clustering.method = <span class="hljs-string">"hierarchical"</span>,
            X.text = round(as.matrix(heat.sum), <span class="hljs-number">2</span>),  
            X.text.size = <span class="hljs-number">4</span>, 
            left.label.size = <span class="hljs-number">0.3</span>,
            left.label.text.size = <span class="hljs-number">4</span>,
            bottom.label.text.size = <span class="hljs-number">4</span>,
            legend = <span class="hljs-literal">FALSE</span>)
}
</code></pre>
<p>In this function, I use the <code>superheat</code> package to draw a heat map. There are many alternatives but <code>superheat</code> is straightforward while offering many options. The first step is to use <code>aggregate()</code> to get the cross-tabs of item means broken out by segment. That single line of code also converts the result to a data frame as <code>superheat</code> expects. (FYI, I drop the initial row using <code>[-1, ]</code> in the conversion because that row contains column names rather than data.)</p>
<p>In the next line, I change the <code>names()</code> of the data frame so they start with “S” for segment, followed by the nominal segment “number”. I add a label for the relative <em>percentage</em> of each. That gives nice labels in <code>superheat</code> with the segment name and its size, such as “<em>S1 (28%)</em>”. BTW, if you had friendly names for the segments — such as “R Pirates” or “UX Mavens” — you could add them here.</p>
<p>After that, the final step is to call <code>superheat</code> to draw the heat map. I won’t explain all of the options in that call, but will mention the three most important ones: (1) I set a <strong>color</strong> <strong>palette</strong> that avoids extreme color intensity. (2) I use “<strong>pretty rows</strong>” and clustering to reorder the results to be grouped by similarity, making it easier to read and interpret the segments. (3) I add the <strong>mean values</strong> themselves — rounded off to 2 decimal places — to the cells using <code>X.text</code>. Without that, it would draw the heat colors but not label the values (which is also a perfectly appropriate choice).</p>
<p>We call the <code>seg.heat()</code> function with the MaxDiff estimates and assigned segments:</p>
<pre><code class="lang-r">seg.heat(md.dat[ , classCols], md.segs)
</code></pre>
<p>Here’s the result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729724203602/7638f3bf-0ca8-4473-ad60-a8d62fff08be.png" alt class="image--center mx-auto" /></p>
<p>What do we see? The five segments are strongly differentiated. I suggest you take a minute to interpret the results … before reading further!</p>
<p>What did you find? Here are a few reflections on each segment from my perspective. I tend to interpret segments according to <em>strongest positive interest</em> first.</p>
<ul>
<li><p><strong>Segment 4</strong> stands out strongly for its interest in R and statistical/data analyses. With roughly 25% of the sample, we can consider it to be a “hardcore Quant” or “data science in UX” segment.</p>
</li>
<li><p>After that, <strong>Segment 5</strong> stands out as a “Core UX” segment, strongly interested in classic mixed-methods UX issues, and not very interested in the stats topics.</p>
</li>
<li><p><strong>Segments 1 and 3</strong> are quite similar, where both are interested in survey methods and choice models. The difference is that Segment 3 is also interested in core UX topics whereas Segment 1 is interested in pricing methods and more advanced choice models. Without looking at the data, my guess is that Segment 1 reflects quant researchers working in <em>marketing</em>, whereas Segment 3 has folks with similar skills and interests but working in <em>UX</em>.</p>
</li>
<li><p>Finally, <strong>Segment 2</strong> is not interested in much … except a segmentation class. That’s fine and is a common pattern — respondents who take the chance to communicate strongly about the one thing they want the most.</p>
</li>
</ul>
<p>I will comment about <em>one thing I almost always see in segmentation projects … but do NOT see in these data</em>: there is often a segment that shows almost <strong>no differentiation</strong> of interest in anything. Such a segment is common in consumer data, and quite likely reflects low quality respondents, speeders, and/or those for whom the survey is inappropriate. It often gives a segment with 30% or more of the sample (I’ve seen as high as 60%). However, the present data come from an especially engaged and high quality panel (Quant UX conference attendees!) so they do not have that problem.</p>
<p>Now, is it actionable? What would we do from a business perspective with these data? From the point of view of the Quant UX Association offering classes, here are some thoughts:</p>
<ul>
<li><p>The results <strong>reinforce</strong> the idea (from Post 1) that — although the <em>average</em> interest in (for example) an <strong>R class</strong> is low, there is a core group of people (Segment 4) who are strongly interested. Now we have a good estimate for that interest, as a segment: 24%</p>
</li>
<li><p>The results also reinforce the interest in a <strong>Segmentation</strong> class. It has strong interest for every segment except Segment 5 … adding up to 90% of the sample.</p>
</li>
<li><p>After that, there is clear interest in <strong>Choice Models</strong> and related topics — in roughly 2nd or 3rd place for three segments — adding up to more than 70% of the sample.</p>
</li>
<li><p><em>Although</em> the size of <strong>Segment 5 is small</strong> (10%), its interest in <strong>UX metrics</strong> topics is extremely clear and crisp. That makes a nice possibility for a clear offering … if the Quant UX Association is able to reach the smallish group in a reasonable way.</p>
</li>
</ul>
<hr />
<h3 id="heading-visualizing-individual-distributions-within-a-segment">Visualizing Individual Distributions within a Segment</h3>
<p>Did you <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-1-data-averages">read post 1</a> in this series? Do you recall the chart of the <em>individual distribution</em> of interest for each class? (That was a density chart for each item.)</p>
<p>We can reuse that plot function now, and ask a similar question: <em>within a particular segment, what is the distribution of interest</em>? Is everyone in the segment alike, or are there strong differences?</p>
<p>Why would we ask that? For two reasons: <strong>to understand the details</strong> of the segment better, and <strong>to gain additional insight before we take action</strong>. For example, suppose we want to offer an R class and we believe that would target the 24% of respondents who fall into Segment 4. We might design the class or message it using insight about the <em>other</em> classes of interest to that segment (such as choice models and psychometrics).</p>
<p>However, if we design or message the class in that way, we would be well-advised not to choose something <strong>polarizing</strong>. It could be that some topic scores high on average with the segment and yet that is driven by a small group. We might be served better by choosing something that is lower on average but is more broadly acceptable. The distribution plot can help us to evaluate such questions.</p>
<p>As presented in <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-1-data-averages">Post #1</a> in this series, here is the function to make an individual-level distribution chart. This is just a repeat from that post (<em>see there for explanation</em>):</p>
<pre><code class="lang-r">cbc.plot &lt;- <span class="hljs-keyword">function</span>(dat, itemCols=<span class="hljs-number">3</span>:ncol(dat), 
                     title = <span class="hljs-string">"Preference estimates: Overall + Individual level"</span>, 
                     meanOrder=<span class="hljs-literal">TRUE</span>) {

  <span class="hljs-comment"># get the mean points so we can plot those over the density plot</span>
  mean.df &lt;- lapply(dat[ , itemCols], mean)

  <span class="hljs-comment"># melt the data for ggplot</span>
  <span class="hljs-keyword">library</span>(reshape2)
  <span class="hljs-comment">#                       vvvv  assumes Sawtooth order;       vvv  (ID in col 1, remove RLH in col 2)</span>
  plot.df &lt;- melt(dat[, c(<span class="hljs-number">1</span>, itemCols)], id.vars=names(dat)[<span class="hljs-number">1</span>])

  <span class="hljs-comment"># get the N of respondents so we can set an appropriate level of point transparency</span>
  p.resp  &lt;- length(unique(plot.df[ , <span class="hljs-number">1</span>]))

  <span class="hljs-comment"># optionally and by default order the results not by column but by mean value</span>
  <span class="hljs-comment"># because ggplot builds from the bottom, we'll reverse them to put max value at the top</span>
  <span class="hljs-comment"># we could use fct_reorder but manually setting the order is straightforward in this case</span>
  <span class="hljs-keyword">if</span> (meanOrder) {
    plot.df$variable &lt;- factor(plot.df$variable, levels = rev(names(mean.df)[order(unlist(mean.df))]))
  }

  <span class="hljs-comment">#### Now : Build the plot</span>
  <span class="hljs-comment"># set.seed(ran.seed)   # optional; points are jittered; setting a seed would make them exactly reproducible</span>
  <span class="hljs-keyword">library</span>(ggplot2)
  <span class="hljs-keyword">library</span>(ggridges)

  <span class="hljs-comment"># build the first layer with the individual distributions</span>
  p &lt;- ggplot(data=plot.df, aes(x=value, y=variable, group=variable)) +
    geom_density_ridges(scale=<span class="hljs-number">0.9</span>, alpha=<span class="hljs-number">0</span>, jittered_points=<span class="hljs-literal">TRUE</span>,
                        rel_min_height=<span class="hljs-number">0.005</span>,
                        position=<span class="hljs-string">"points_sina"</span>,
                        <span class="hljs-comment"># set individual point alphas in inverse proportion to sample size</span>
                        point_color = <span class="hljs-string">"blue"</span>, point_alpha=<span class="hljs-number">1</span>/sqrt(p.resp),
                        point_size=<span class="hljs-number">2.5</span>) +
    <span class="hljs-comment"># reverse y axis to match attribute order from top</span>
    scale_y_discrete(limits=rev) +                                                   
    ylab(<span class="hljs-string">"Level"</span>) + xlab(<span class="hljs-string">"Relative preference (blue=individuals, red=average)"</span>) +
    ggtitle(title) +
    theme_minimal()

  <span class="hljs-comment"># now add second layer to plot with the means of each item distribution</span>
  <span class="hljs-keyword">for</span> (i <span class="hljs-keyword">in</span> <span class="hljs-number">1</span>:length(mean.df)) {
    <span class="hljs-keyword">if</span> (meanOrder) {
      <span class="hljs-comment"># if we're drawing them in mean order, get the right one same as above</span>
      p &lt;- p + geom_point(x=mean.df[[rev(order(unlist(mean.df)))[i]]], 
                          y=length(mean.df)-i+<span class="hljs-number">1</span>, colour=<span class="hljs-string">"tomato"</span>,      <span class="hljs-comment"># adjust y axis because axis is reversed above</span>
                          alpha=<span class="hljs-number">0.5</span>, size=<span class="hljs-number">2.0</span>, shape=<span class="hljs-number">0</span>, inherit.aes=<span class="hljs-literal">FALSE</span>)

    } <span class="hljs-keyword">else</span> {
      p &lt;- p + geom_point(x=mean.df[[i]], 
                          y=length(mean.df)-i+<span class="hljs-number">1</span>, colour=<span class="hljs-string">"tomato"</span>,      <span class="hljs-comment"># adjust y axis because axis is reversed above</span>
                          alpha=<span class="hljs-number">0.5</span>, size=<span class="hljs-number">2.0</span>, shape=<span class="hljs-number">0</span>, inherit.aes=<span class="hljs-literal">FALSE</span>)
    }
  }
  p
}
</code></pre>
<p>We can reuse that function exactly as is, except we plot only the respondents in Segment 4 by subsetting the data appropriately (<code>md.segs == 4</code>):</p>
<pre><code class="lang-r">cbc.plot(md.dat[md.segs == <span class="hljs-number">4</span>, ], itemCols = classCols) + 
  ylab(<span class="hljs-string">"Quant Course Offering"</span>) +
  ggtitle(<span class="hljs-string">"Class interest: Segment 4 Only (R)"</span>)
</code></pre>
<p>The result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729725989889/682385fe-2f21-433f-9657-9e250fb18dd6.png" alt class="image--center mx-auto" /></p>
<p>In this chart we see the strong cluster for the R programming interest again, not only as the #1 item on average but showing a strong group of individuals with exceptional interest on the right hand side of the distribution.</p>
<p>After that, <strong>Choice Modeling</strong> appears to be a good secondary emphasis — not only is it #2 in position, but the distribution is tightly grouped and <strong>non-polarizing</strong>. Every respondent had interest greater than 0 (the “anchor” position for positive interest). Psychometrics, Causal Models, and Advanced Choice are all similar.</p>
<p>Farther down, we see that Metrics Sprints, Surveys, and Pricing are fairly highly <strong>polarizing</strong> within this segment: there are a few people with strong interest but also those with strong disinterest.</p>
<p>From a business perspective, that reinforces the idea that we might offer an R class with a secondary emphasis on choice modeling (<em>like this post!</em>) or psychometrics. And it suggests to avoid a few topics that are polarizing; they would lack appeal to too many participants (assuming our strategy is to go after the general audience).</p>
<p>Again, <strong>the key point of such deep dives is to understand the preference structure in more detail, we we can consider actions</strong> in design and/or promotion of our offerings. In this case, it could help with constructing the content of a class, with targeting respondents for interest in it, and describing it.</p>
<p><em>This is a good time to add one important caveat</em> to all of this, an obvious point to researchers and yet one that is easy to forget: <em>such results come from a particular sample, and we have to limit our interpretation to that sample (or its implied population)</em>. In this case, the results come from attendees of Quant UX Con … and Quant UX Con promoted many of the same topics (R, choice models, metrics, and the like). So to some extent it is no surprise that this sample would have these patterns of interest! Yet, at the same time, the data deepen our understanding of the sample, and our confidence in potential strategic actions.</p>
<hr />
<h3 id="heading-but-again-there-is-no-magic-segmentation">But Again: There is no Magic Segmentation</h3>
<p>Let’s take a look again at a fact I mentioned earlier: that there are hundreds of clustering methods — multiplied by dozens or even hundreds of ways to apply each of them. For example, with each one we might select among multiple distance metrics for observation similarity or multiple way to choose a number of clusters.</p>
<p>To take one example, we have already seen the <code>mclust</code> package that finds model-based multivariate normal mixtures (i.e., clusters). As we saw above, it suggests that the data represent 1 cluster. However, we can force it to fit a 5 cluster model, by setting the parameter <code>G</code>:</p>
<pre><code class="lang-r"><span class="hljs-comment"># we could force a 5-segment solution (for example) in Mclust</span>
<span class="hljs-keyword">library</span>(mclust)
md.mc &lt;- Mclust(md.dat[ , classCols], G=<span class="hljs-number">5</span>)
</code></pre>
<p>Now we can compare the solution from <code>mclust</code> to the solution from <code>NbClust</code> above:</p>
<pre><code class="lang-r"><span class="hljs-comment"># compare the mclust solution to the previous NbClust solution</span>
table(md.mc$classification, md.segs)
</code></pre>
<p>Here is the comparison table, which presents <code>NbClust</code> segment assignments in the columns, cross-tabbed by <code>mclust</code> assignments in the rows:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729815694693/e50726bb-8af8-4b21-b49e-d41838225971.png" alt class="image--center mx-auto" /></p>
<p>We can’t compare the segment numbers — such as Segment 1 in each solution — because the assignment labels are nominal and have no order or meaning in themselves. For example, most of the respondents (26/31) who are labeled as “Segment 5” in <code>NbClust</code> fall into the group called “Segment 4” in the <code>mclust</code> solution.</p>
<p>However, if it were the case that only the <em>labels</em> were different, then all of the respondents in one column would fall into a single row. When they don’t, then it says that respondents assigned to one group in <code>NbClust</code> fall into multiple groups in <code>mclust</code>. In other words, the two methods find different groupings of respondents.</p>
<p>With that perspective, <strong>we can easily see signs of substantial disagreement between the two 5-cluster solutions</strong>. In Segments 1, 2, and 3 from <code>NbClust</code> — and segments 2, 3, and 5 from <code>mclust</code> — fewer than 50% of the observations from any segment are assigned to any single group in the other solution. In other words, the solutions disagree on a <em>majority</em> of segment assignments.</p>
<p>We might compute agreement by hand. Here’s one way. Column 4 maps best to the label of “3” in the other solution, capturing 55 respondents. Column 5 maps to “4” (26 respondents). Column 3 <em>would</em> map to “3” but that is already taken by Column 4, so we’ll map it to the second best assignment, “2” (N=21). That leaves Column 1 mapping to “5” (N=24) and Column 2 mapping to “1” (N=23). Adding them up, (55+26+21+24+23) / 308 = <strong>48% raw agreement</strong>. That tells us something, but it would be better to adjust that with respect to chance (for example, if one cluster is large, then agreement could be “high” just by assigning all 100% of respondents to it!)</p>
<p>The <strong>adjusted Rand index</strong> is one method to determine congruence between cluster solutions, relative to base rate. We check that:</p>
<pre><code class="lang-r"><span class="hljs-comment"># Rand index for the degree of agreement between them</span>
adjustedRandIndex(md.mc$classification, md.segs)
</code></pre>
<p>The answer is ARI = 0.186 … which means that <strong>the 5-segment solutions from</strong> <code>NbClust</code> <strong>and</strong> <code>mclust</code> <strong>agree only slightly better than random chance</strong> (about “18% better than random” to use a heuristic explanation of the index). Put differently, the two statistical models often assign respondents to quite different segments.</p>
<p>We can also draw a UMAP plot with the new <code>mclust</code> segment assignments:</p>
<pre><code class="lang-r"><span class="hljs-comment"># UMAP plot of the mclust assignments</span>
set.seed(<span class="hljs-number">98107</span>)
<span class="hljs-comment"># the same segments but in random order</span>
umap.dat$mclustSeg &lt;- factor(md.mc$classification)
<span class="hljs-comment"># plot those</span>
ggplot(umap.dat, aes(x = X1, y = X2, colour = mclustSeg)) +
  geom_point() +
  xlab(<span class="hljs-string">"High order dimension 1"</span>) + ylab(<span class="hljs-string">"High order dimension 2"</span>) +
  theme_minimal()+
  ggtitle(<span class="hljs-string">"Dimensional map with mclust segments"</span>)
</code></pre>
<p>Here’s the new plot:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729816624402/18ad8954-9a09-488a-a498-f0ac8d29b22a.png" alt class="image--center mx-auto" /></p>
<p>The result shows segments that are much less cleanly separated than the <code>NbClust</code> segments, with respect to the UMAP dimensions that we plotted above. It has clear clusters, but not as clear as the chart above. Segment 3 is relatively separate from the others, but segments 1, 2, 4, and 5 have a lot of mixing with near neighbors assigned to other segments.</p>
<p><strong>That doesn’t mean that</strong> <code>NbClust</code> <strong>is better than</strong> <code>mclust</code> … because UMAP dimensions are not the only method to assess segment clarity or utility. <strong>It only shows that the two solutions are different</strong> … <em>because there is no single, canonical, or “best” segmentation method</em>.</p>
<p>That’s not the end of the story, however! We need to look at the implications.</p>
<hr />
<h3 id="heading-and-yet-it-may-be-useful">And Yet It May Be Useful</h3>
<p><strong>Beyond individual respondent assignment, there is a more important question</strong>: <em>do the strategic implications differ</em> between the two solutions? Let’s look at a heat map for the new <code>mclust</code> segments; our reusable function above makes it easy; we just pass those segment assignments to it (<code>md.mc$classification</code>):</p>
<pre><code class="lang-r"><span class="hljs-comment"># plot the 5-segment mclust solution as a heat map</span>
seg.heat(md.dat[ , classCols], md.mc$classification)
</code></pre>
<p>Here’s the plot:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729894992149/706111a8-af1e-45d9-8fcb-e9573fb51505.png" alt class="image--center mx-auto" /></p>
<p>To interpret it, I have copied below the comments I made above about the <code>NbClust</code> segments … and added commentary that compares each segment to the new, and — on an <em>individual level</em> — substantially different <code>mclust</code> segment:</p>
<ul>
<li><p><strong>Segment 4</strong> [NbClust] stands out strongly for its interest in R and statistical/data analyses. With roughly 25% of the sample, we can consider it to be a “hardcore Quant” or “data science in UX” segment. <strong>\===&gt;</strong> <em>That aligns with Segment 3</em> in the new solution, although the segment is somewhat larger (34%)</p>
</li>
<li><p>After that, <strong>Segment 5</strong> [NbClust] stands out as a “Core UX” segment, strongly interested in classic mixed-methods UX issues, and not very interested in the stats topics. <strong>\===&gt;</strong> <em>That aligns with Segment 4</em> in the new solution, similarly sized.</p>
</li>
<li><p><strong>Segments 1 and 3</strong> [NbClust] are quite similar, where both are interested in survey methods and choice models. Segment 3 is interested in core UX topics whereas Segment 1 is interested in pricing methods and more advanced choice models. <strong>\===&gt;</strong> <em>These are generally similar to Segments 2 and 5</em> in the new solution.</p>
</li>
<li><p>Finally, <strong>Segment 2</strong> [NbClust] is not interested in much … except a segmentation class. That’s fine and is a common pattern — respondents who take the chance to communicate strongly about the one thing they want the most. <strong>\===&gt;</strong> <em>That looks a lot like Segment 1</em> in the new solution.</p>
</li>
</ul>
<p>In short, although the segmentation solutions differ substantially according to which <em>respondents</em> they places into each segment, <strong>the two methods ended up finding quite similar segments from a strategic point of view</strong>! The business implications — e.g., for R/stats classes, high interest in segmentation, a core UX segment, and so forth — appear to be nearly identical between the two solutions. That happy result reassures us about the plausibility and statistical stability of the solutions.</p>
<p>Put differently, although the assignment of each <em>respondent</em> to a segment is uncertain here and often disagrees, the segment <em>averages</em> end up being quite similar between the two methods. That is a general observation in choice model data (and TBH it also elates to a bedrock principle of statistics: we do stats to understand a population better by sampling and averaging! [among other ways])</p>
<p>However, I will add a caution: <strong>the strong similarity of the results here is an N=1 occurrence</strong>. <em>There is no guarantee that two segmentation methods will give such similar results</em>. In fact, given the low agreement we see for individual respondent assignment, <strong>my general presumption is that any two clustering solutions are somewhat likely to disagree in their strategic implications</strong>. We must do work to get beyond that, through careful interpretation and iteration, and by confirming and triangulating results!</p>
<hr />
<h3 id="heading-conclusion-and-a-final-warning">Conclusion and a Final Warning</h3>
<p>One point I have not yet made is this: <strong>an extremely common problem in cluster analysis is using far too many variables</strong>. In this post, I used 14 variables and I think that’s a perfectly reasonable size. It helps that they come from the same kind of observation (choice model) and estimation method (HB utilities). Yet many analysts attempt to cluster data sets with 50, 100, or 300 variables per respondent.</p>
<p>The problem with that is that <strong>clustering works by evaluating how similar respondents are to one another … and with so many variables, no one is similar!</strong> It is crucial to prune down the set of “basis variables” to be both <em>few</em> and of <em>known importance</em>. That’s a topic for more extended discussion.</p>
<p>Meanwhile, I hope this post demonstrates that:</p>
<ol>
<li><p>It is quite possible to <strong>find useful segments</strong> in data</p>
</li>
<li><p>… <strong>even when</strong> a statistical model claims there are “no segments”</p>
</li>
<li><p>… while using <strong>general methods</strong> that work across many kinds of data</p>
</li>
<li><p>… and that provide good opportunities to <strong>cross-check our work</strong> as we go</p>
</li>
<li><p>… while building <strong>reusable functions</strong> along the way</p>
</li>
<li><p>… as long as we are cautious and <strong>avoid common pitfalls</strong>.</p>
</li>
</ol>
<p>I hope you’ve found this interesting and helpful for your own analyses!</p>
<hr />
<h3 id="heading-learning-more">Learning More</h3>
<p>If you’d like to learn more about segmentation, here are some places to start:</p>
<ul>
<li><p>For <strong>general segmentation</strong>, see the chapters on clustering and classification in the <a target="_blank" href="https://www.amazon.com/Marketing-Research-Analytics-Use/dp/3030143155">R book</a> and the <a target="_blank" href="https://www.amazon.com/Python-Marketing-Research-Analytics-Schwarz/dp/3030497194">Python book</a>.</p>
</li>
<li><p>… or sign up for a <strong>class</strong>! In January 2025 there will be an <a target="_blank" href="https://events.ringcentral.com/events/segmentation-master-class-jan2025"><strong>online Segmentation Class</strong> from the Quant UX Association</a> taught by surveys expert Keith Chrzan</p>
</li>
<li><p>To learn about <strong>choice models</strong> in general, see discussion of conjoint analysis in the <a target="_blank" href="https://www.amazon.com/Marketing-Research-Analytics-Use/dp/3030143155">R book</a> and the <a target="_blank" href="https://www.amazon.com/Python-Marketing-Research-Analytics-Schwarz/dp/3030497194">Python book</a>, and of MaxDiff in the Quant UX book.</p>
</li>
<li><p>… or join me for an upcoming offering of the <a target="_blank" href="https://www.quantuxcon.org/classes">Choice Modeling Master Class</a>!</p>
</li>
<li><p>For <strong>latent class models</strong> specific to conjoint analysis and MaxDiff, check out many papers over the years in the <a target="_blank" href="https://sawtoothsoftware.com/resources/events/conferences">archives of the Sawtooth Software Conference</a> (scroll down for the PDF archives by year).</p>
</li>
</ul>
<hr />
<h3 id="heading-all-the-code">All the Code</h3>
<p>As always, here is all of the R code from this post compiled in one place. You can grab it with the <em>copy</em> icon and paste into your favorite R code editor.</p>
<pre><code class="lang-r"><span class="hljs-comment">##### Post 3 - Segmentation (clustering)</span>

<span class="hljs-comment"># get the data; repeating here for blog post 3, see post 1 for details</span>
<span class="hljs-keyword">library</span>(openxlsx)
md.dat &lt;- read.xlsx(<span class="hljs-string">"https://quantuxbook.com/misc/QUX%20Survey%202024%20-%20Future%20Classes%20-%20MaxDiff%20Individual%20raw%20scores.xlsx"</span>)   <span class="hljs-comment"># </span>
md.dat$Anchor &lt;- <span class="hljs-literal">NULL</span>
names(md.dat)[<span class="hljs-number">3</span>:<span class="hljs-number">16</span>] &lt;- c(<span class="hljs-string">"Choice Models"</span>,  <span class="hljs-string">"Surveys"</span>,       <span class="hljs-string">"Log Sequences"</span>,    <span class="hljs-string">"Psychometrics"</span>, 
                         <span class="hljs-string">"R Programming"</span>,  <span class="hljs-string">"Pricing"</span>,       <span class="hljs-string">"UX Metrics"</span>,       <span class="hljs-string">"Bayes Stats"</span>,
                         <span class="hljs-string">"Text Analytics"</span>, <span class="hljs-string">"Causal Models"</span>, <span class="hljs-string">"Interviewer-ing"</span>,  <span class="hljs-string">"Advanced Choice"</span>, 
                         <span class="hljs-string">"Segmentation"</span>,   <span class="hljs-string">"Metrics Sprints"</span>)
classCols &lt;- <span class="hljs-number">3</span>:ncol(md.dat)    <span class="hljs-comment"># generally, Sawtooth exported utilities start in column 3</span>


<span class="hljs-comment">##### 5. Number of segments?</span>

<span class="hljs-comment"># mclust uses model-based fit estimates to determine best number of latent clusters</span>
<span class="hljs-keyword">library</span>(mclust)
md.mc &lt;- Mclust(md.dat[ , classCols])
summary(md.mc)     <span class="hljs-comment"># best answer is only 1 "segment", multivariate normal within it</span>

<span class="hljs-comment"># initial visualization with UMAP</span>

<span class="hljs-keyword">library</span>(umap)
<span class="hljs-comment"># get umap representation of the individual interest data</span>
umap.config &lt;- umap.defaults
umap.config$random_state &lt;- <span class="hljs-number">98101</span>

umap.sol &lt;- umap(md.dat[ , classCols], config = umap.config)
umap.dat &lt;- data.frame(umap.sol$layout)
<span class="hljs-comment"># review the kind of data we have</span>
str(umap.dat)

<span class="hljs-comment"># plot it</span>
ggplot(umap.dat, aes(x = X1, y = X2)) +
  geom_point() +
  xlab(<span class="hljs-string">"High order dimension 1"</span>) + ylab(<span class="hljs-string">"High order dimension 2"</span>) +
  theme_minimal()


<span class="hljs-comment">##### 6. Find number of clusters and initial solution</span>

<span class="hljs-keyword">library</span>(NbClust)
md.nbc &lt;- NbClust(md.dat[ , classCols],
                  min.nc = <span class="hljs-number">2</span>, max.nc = <span class="hljs-number">10</span>,
                  method = <span class="hljs-string">"ward.D"</span>, 
                  distance = <span class="hljs-string">"euclidean"</span>)

<span class="hljs-comment"># we'll try a 5 cluster solution instead of 2</span>
md.nbc &lt;- NbClust(md.dat[ , classCols],
                  min.nc = <span class="hljs-number">5</span>, max.nc = <span class="hljs-number">10</span>,
                  method = <span class="hljs-string">"ward.D"</span>, 
                  distance = <span class="hljs-string">"euclidean"</span>)

md.segs &lt;- md.nbc$Best.partition
table(md.segs)
proportions(table(md.segs))


<span class="hljs-comment">##### 7. Plot umap with segment membership coded</span>

<span class="hljs-comment"># add the 5-segment membership to the UMAP data</span>
umap.dat$Segment &lt;- factor(paste0(<span class="hljs-string">"S"</span>, md.segs))

<span class="hljs-comment"># plot it with segments</span>
ggplot(umap.dat, aes(x = X1, y = X2, colour = Segment)) +
  geom_point() +
  xlab(<span class="hljs-string">"High order dimension 1"</span>) + ylab(<span class="hljs-string">"High order dimension 2"</span>) +
  theme_minimal() +
  ggtitle(<span class="hljs-string">"Dimensional map with 5 segments"</span>)

<span class="hljs-comment"># make sure we're not fooling ourselves ...</span>
<span class="hljs-comment"># what would it look like if the segments were random?</span>
set.seed(<span class="hljs-number">98107</span>)
<span class="hljs-comment"># the same segments but in random order</span>
umap.dat$RndSeg &lt;- sample(umap.dat$Segment)
<span class="hljs-comment"># plot those</span>
ggplot(umap.dat, aes(x = X1, y = X2, colour = RndSeg)) +
  geom_point() +
  xlab(<span class="hljs-string">"High order dimension 1"</span>) + ylab(<span class="hljs-string">"High order dimension 2"</span>) +
  theme_minimal()+
  ggtitle(<span class="hljs-string">"Dimensional map with RANDOM segments"</span>)


<span class="hljs-comment">##### 8. Plot heat map for segments</span>

<span class="hljs-comment"># function to plot average values by segment</span>
seg.heat &lt;- <span class="hljs-keyword">function</span>(dat, segs) {
  <span class="hljs-keyword">library</span>(superheat)
  <span class="hljs-comment"># aggregate mean value by segment</span>
  heat.sum &lt;- data.frame(t(aggregate(. ~ segs, data=dat, mean))[-<span class="hljs-number">1</span>, ])
  <span class="hljs-comment"># make the column names reflect the segment number and size (%)</span>
  names(heat.sum) &lt;- paste0(<span class="hljs-string">"S"</span>, <span class="hljs-number">1</span>:max(segs), <span class="hljs-string">" ("</span>, 
                            round(prop.table(table(segs)), <span class="hljs-number">2</span>)*<span class="hljs-number">100</span>, <span class="hljs-string">"%)"</span>)
  <span class="hljs-comment"># draw it with superheat</span>
  superheat(heat.sum, 
            heat.pal = c(<span class="hljs-string">"red3"</span>, <span class="hljs-string">"white"</span>, <span class="hljs-string">"green3"</span>),
            grid.hline.col = <span class="hljs-string">"white"</span>, grid.vline.col = <span class="hljs-string">"white"</span>,
            pretty.order.rows = <span class="hljs-literal">TRUE</span>,
            clustering.method = <span class="hljs-string">"hierarchical"</span>,
            X.text = round(as.matrix(heat.sum), <span class="hljs-number">2</span>),  
            X.text.size = <span class="hljs-number">4</span>, 
            left.label.size = <span class="hljs-number">0.3</span>,
            left.label.text.size = <span class="hljs-number">4</span>,
            bottom.label.text.size = <span class="hljs-number">4</span>,
            legend = <span class="hljs-literal">FALSE</span>)
}

<span class="hljs-comment"># plot the segmentation solution</span>
seg.heat(md.dat[ , classCols], md.segs)


<span class="hljs-comment">##### 9. Plot distribution of interest *within* a segment</span>
<span class="hljs-comment"># check the data briefly ; not discussed in the blog post</span>
summary(md.dat[md.segs==<span class="hljs-number">4</span>, ])

<span class="hljs-comment"># reuse our plot function -- copied here from the part 1 post</span>
<span class="hljs-comment"># see the part 1 blog post for an explanation of this function</span>
cbc.plot &lt;- <span class="hljs-keyword">function</span>(dat, itemCols=<span class="hljs-number">3</span>:ncol(dat), 
                     title = <span class="hljs-string">"Preference estimates: Overall + Individual level"</span>, 
                     meanOrder=<span class="hljs-literal">TRUE</span>) {

  <span class="hljs-comment"># get the mean points so we can plot those over the density plot</span>
  mean.df &lt;- lapply(dat[ , itemCols], mean)

  <span class="hljs-comment"># melt the data for ggplot</span>
  <span class="hljs-keyword">library</span>(reshape2)
  <span class="hljs-comment">#                       vvvv  assumes Sawtooth order;       vvv  (ID in col 1, remove RLH in col 2)</span>
  plot.df &lt;- melt(dat[, c(<span class="hljs-number">1</span>, itemCols)], id.vars=names(dat)[<span class="hljs-number">1</span>])

  <span class="hljs-comment"># get the N of respondents so we can set an appropriate level of point transparency</span>
  p.resp  &lt;- length(unique(plot.df[ , <span class="hljs-number">1</span>]))

  <span class="hljs-comment"># optionally and by default order the results not by column but by mean value</span>
  <span class="hljs-comment"># because ggplot builds from the bottom, we'll reverse them to put max value at the top</span>
  <span class="hljs-comment"># we could use fct_reorder but manually setting the order is straightforward in this case</span>
  <span class="hljs-keyword">if</span> (meanOrder) {
    plot.df$variable &lt;- factor(plot.df$variable, levels = rev(names(mean.df)[order(unlist(mean.df))]))
  }

  <span class="hljs-comment">#### Now : Build the plot</span>
  <span class="hljs-comment"># set.seed(ran.seed)   # optional; points are jittered; setting a seed would make them exactly reproducible</span>
  <span class="hljs-keyword">library</span>(ggplot2)
  <span class="hljs-keyword">library</span>(ggridges)

  <span class="hljs-comment"># build the first layer with the individual distributions</span>
  p &lt;- ggplot(data=plot.df, aes(x=value, y=variable, group=variable)) +
    geom_density_ridges(scale=<span class="hljs-number">0.9</span>, alpha=<span class="hljs-number">0</span>, jittered_points=<span class="hljs-literal">TRUE</span>,
                        rel_min_height=<span class="hljs-number">0.005</span>,
                        position=<span class="hljs-string">"points_sina"</span>,
                        <span class="hljs-comment"># set individual point alphas in inverse proportion to sample size</span>
                        point_color = <span class="hljs-string">"blue"</span>, point_alpha=<span class="hljs-number">1</span>/sqrt(p.resp),
                        point_size=<span class="hljs-number">2.5</span>) +
    <span class="hljs-comment"># reverse y axis to match attribute order from top</span>
    scale_y_discrete(limits=rev) +                                                   
    ylab(<span class="hljs-string">"Level"</span>) + xlab(<span class="hljs-string">"Relative preference (blue=individuals, red=average)"</span>) +
    ggtitle(title) +
    theme_minimal()

  <span class="hljs-comment"># now add second layer to plot with the means of each item distribution</span>
  <span class="hljs-keyword">for</span> (i <span class="hljs-keyword">in</span> <span class="hljs-number">1</span>:length(mean.df)) {
    <span class="hljs-keyword">if</span> (meanOrder) {
      <span class="hljs-comment"># if we're drawing them in mean order, get the right one same as above</span>
      p &lt;- p + geom_point(x=mean.df[[rev(order(unlist(mean.df)))[i]]], 
                          y=length(mean.df)-i+<span class="hljs-number">1</span>, colour=<span class="hljs-string">"tomato"</span>,      <span class="hljs-comment"># adjust y axis because axis is reversed above</span>
                          alpha=<span class="hljs-number">0.5</span>, size=<span class="hljs-number">2.0</span>, shape=<span class="hljs-number">0</span>, inherit.aes=<span class="hljs-literal">FALSE</span>)

    } <span class="hljs-keyword">else</span> {
      p &lt;- p + geom_point(x=mean.df[[i]], 
                          y=length(mean.df)-i+<span class="hljs-number">1</span>, colour=<span class="hljs-string">"tomato"</span>,      <span class="hljs-comment"># adjust y axis because axis is reversed above</span>
                          alpha=<span class="hljs-number">0.5</span>, size=<span class="hljs-number">2.0</span>, shape=<span class="hljs-number">0</span>, inherit.aes=<span class="hljs-literal">FALSE</span>)
    }
  }
  p
}

<span class="hljs-comment"># use our plot function with only the Segment 4 data</span>
cbc.plot(md.dat[md.segs==<span class="hljs-number">4</span>, ], itemCols=classCols) + 
  ylab(<span class="hljs-string">"Quant Course Offering"</span>) +
  ggtitle(<span class="hljs-string">"Class interest: Segment 4 Only (R)"</span>)

<span class="hljs-comment">##### 10. There is no magic answer in clustering</span>

<span class="hljs-comment"># we could force a 5-segment solution (for example) in Mclust</span>
<span class="hljs-keyword">library</span>(mclust)
md.mc &lt;- Mclust(md.dat[ , classCols], G=<span class="hljs-number">5</span>)

<span class="hljs-comment"># compare the mclust solution to the previous NbClust solution</span>
table(md.mc$classification, md.segs)
<span class="hljs-comment"># Rand index for the degree of agreement between them</span>
adjustedRandIndex(md.mc$classification, md.segs)

<span class="hljs-comment"># UMAP plot of the mclust assignments</span>
set.seed(<span class="hljs-number">98107</span>)
<span class="hljs-comment"># the same segments but in random order</span>
umap.dat$mclustSeg &lt;- factor(md.mc$classification)
<span class="hljs-comment"># plot those</span>
ggplot(umap.dat, aes(x = X1, y = X2, colour = mclustSeg)) +
  geom_point() +
  xlab(<span class="hljs-string">"High order dimension 1"</span>) + ylab(<span class="hljs-string">"High order dimension 2"</span>) +
  theme_minimal()+
  ggtitle(<span class="hljs-string">"Dimensional map with mclust segments"</span>)

<span class="hljs-comment"># plot the 5-segment mclust solution as a heat map</span>
seg.heat(md.dat[ , classCols], md.mc$classification)

<span class="hljs-comment"># and plot the individual distributions for the "R" segment, similar to above</span>
<span class="hljs-comment"># not discussed in the post; just illustrating the reusable plot function :)</span>
cbc.plot(md.dat[md.mc$classification==<span class="hljs-number">3</span>, ], itemCols=classCols) + 
  ylab(<span class="hljs-string">"Quant Course Offering"</span>) +
  ggtitle(<span class="hljs-string">"Class interest: Mclust R Segment Only"</span>)
</code></pre>
<p><a target="_blank" href="https://notbyai.fyi"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746999945541/9d224843-9e9f-44cc-98c5-276915794420.png" alt class="image--center mx-auto" /></a></p>
]]></content:encoded></item><item><title><![CDATA[Individual Scores in Choice Models, Part 2: Correlations among Items]]></title><description><![CDATA[Today’s post continues my examination of working with individual-level scores from choice modeling surveys, such as MaxDiff and Conjoint Analysis surveys.
In this post, we’ll examine and visualize correlation patterns among the items. This also sets ...]]></description><link>https://quantuxblog.com/individual-scores-in-choice-models-part-2-correlations-among-items</link><guid isPermaLink="true">https://quantuxblog.com/individual-scores-in-choice-models-part-2-correlations-among-items</guid><category><![CDATA[maxdif]]></category><category><![CDATA[quantux]]></category><category><![CDATA[R Language]]></category><category><![CDATA[uxresearch]]></category><category><![CDATA[marketing research]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Mon, 28 Oct 2024 15:39:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/xnqVGsbXgV4/upload/c2507cbb0de07f34f70c7d910ee7a096.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Today’s post continues my examination of working with individual-level scores from choice modeling surveys, such as MaxDiff and Conjoint Analysis surveys.</p>
<p>In this post, we’ll examine and visualize <em>correlation</em> patterns among the items. This also sets up another opportunity (as if I needed one!) to discuss why “statistical significance” in such data is usually unimportant. We can ignore “significance” with the data and yet still learn much from them.</p>
<p>If you haven’t seen it, you could <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-1-data-averages">review Part 1 of this series</a>. It describes the data and presents basic data visualization code. Here I pick up where that post ended and discuss additional analyses. As usual, I share and discuss R code along the way; and compile all the code at the end.</p>
<hr />
<h3 id="heading-first-get-the-data">First: Get the Data</h3>
<p>If you ran the code in <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-1-data-averages">Part 1</a> then you may have the data. You can refer there for explanation; meanwhile, the following code obtains the data needed for this post.</p>
<p>Briefly, this code downloads MaxDiff scores in Excel format from a URL, and does minor clean up. This real data set estimates N=308 individuals’ interest in taking various Quant UX classes, such classes on R, Choice Models, and Segmentation.</p>
<pre><code class="lang-r"><span class="hljs-comment"># get the data; repeating here for blog post 2, see post 1 for details</span>
<span class="hljs-keyword">library</span>(openxlsx)
md.dat &lt;- read.xlsx(<span class="hljs-string">"https://quantuxbook.com/misc/QUX%20Survey%202024%20-%20Future%20Classes%20-%20MaxDiff%20Individual%20raw%20scores.xlsx"</span>)   <span class="hljs-comment"># </span>
md.dat$Anchor &lt;- <span class="hljs-literal">NULL</span>          <span class="hljs-comment"># remove the anchor item that is a fixed value of 0</span>
names(md.dat)[<span class="hljs-number">3</span>:<span class="hljs-number">16</span>] &lt;- c(<span class="hljs-string">"Choice Models"</span>,  <span class="hljs-string">"Surveys"</span>,       <span class="hljs-string">"Log Sequences"</span>,    <span class="hljs-string">"Psychometrics"</span>, 
                         <span class="hljs-string">"R Programming"</span>,  <span class="hljs-string">"Pricing"</span>,       <span class="hljs-string">"UX Metrics"</span>,       <span class="hljs-string">"Bayes Stats"</span>,
                         <span class="hljs-string">"Text Analytics"</span>, <span class="hljs-string">"Causal Models"</span>, <span class="hljs-string">"Interviewer-ing"</span>,  <span class="hljs-string">"Advanced Choice"</span>, 
                         <span class="hljs-string">"Segmentation"</span>,   <span class="hljs-string">"Metrics Sprints"</span>)
classCols &lt;- <span class="hljs-number">3</span>:ncol(md.dat)    <span class="hljs-comment"># generally, Sawtooth exported utilities start in column 3</span>
</code></pre>
<hr />
<h3 id="heading-correlation-analysis-basic">Correlation Analysis: Basic</h3>
<p>After reviewing and comparing the preferences for each class, as we did in the previous <a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-1-data-averages">Post 1</a>, a next question might be whether there are patterns among the potential class offerings.</p>
<p>Specifically, when respondents are interested in <em>one class</em>, what does that say about their interest in <em>other classes</em>? Are there combinations of classes that are associated with one another?</p>
<p>There are several ways to examine such patterns (see, for example, Chapters 4, 11, and 12 in the <a target="_blank" href="https://www.amazon.com/Marketing-Research-Analytics-Use/dp/3030143155">R book</a>; or Chapters 4, 9, and 10 in the <a target="_blank" href="https://www.amazon.com/Python-Marketing-Research-Analytics-Schwarz/dp/3030497194">Python book</a>). Here I’ll demonstrate visualization of a Pearson’s <em>r</em> correlation matrix.</p>
<blockquote>
<p><strong>Background</strong>: In a nutshell, Pearson’s <strong>r</strong> varies between -1.0 and +1.0. A value of 0.0 indicates no relationship between a pair of variables — in this case, it would mean there is no relation between interest in one class and interest in another. As <strong>r</strong> approaches +1.0 there is a stronger and stronger pattern of interest in the same direction for both classes together. As <strong>r</strong> approaches -1.0, it means that interest in one class is increasingly associated with <strong>disinterest</strong> in the other class.</p>
</blockquote>
<p>The R code to visualize a general correlation matrix is not complex. First, we calculate the correlation matrix using <code>cor()</code>. Then we plot it with the <code>corrplot</code> library:</p>
<pre><code class="lang-r">md.cor &lt;- cor(md.dat[ , classCols])

<span class="hljs-comment"># basic plot</span>
<span class="hljs-keyword">library</span>(corrplot)
corrplot(md.cor)
</code></pre>
<p>Here’s the result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729297243137/6b2e66ff-458e-4e09-b960-1cf7e77cfed6.png" alt class="image--center mx-auto" /></p>
<p><strong>Wow!</strong> That’s a great chart for 3 lines of code. We immediately see some interesting combinations. For example, interest in <em>Choice Models</em> and <em>Advanced Choice Models</em> go together; as does interest in <em>UX Metrics</em> and <em>Metrics Sprints</em>. On the other hand, interest in <em>Choice Models</em> is negatively associated with Quant <em>Interviewer</em> training.</p>
<p>If you’ve read any of my R posts before, <strong>you know what’s next: we can do better</strong>!</p>
<hr />
<h3 id="heading-correlation-analysis-improving-the-visualization">Correlation Analysis: Improving the Visualization</h3>
<p>One problem with the correlation plot above is that it is unordered with respect to the estimates themselves; items are given merely in the order as listed in the data.</p>
<p><strong>A more useful correlation plot reorders the items in terms of similarity</strong> (i.e., clustering). Then we can see whether there are groups of items with similar patterns. Even better, we might draw boxes around those groups to highlight them.</p>
<p>That’s also straightforward in <code>corrplot</code>. Here’s the code:</p>
<pre><code class="lang-r"><span class="hljs-comment"># correlations with clustering</span>
corrplot(md.cor, method = <span class="hljs-string">"ellipse"</span>,
         order = <span class="hljs-string">"hclust"</span>, addrect = <span class="hljs-number">4</span>)
</code></pre>
<p>To explain the options, <code>method=”ellipse”</code> converts the circles into ellipses that indicate strength of association (by narrowness) and direction (by slope). By using <code>order=”hclust”</code>, we ask <code>corrplot</code> to cluster the items for similarity. The <code>addrect=4</code> option draws 4 boxes to highlight the clusters. Generally you pick that number according to what makes sense with some trial and error (but see below for more discussion of finding a “good” number of clusters!)</p>
<p>Here’s the result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729297949272/528de337-21d6-488f-a2f7-985f4c065d53.png" alt class="image--center mx-auto" /></p>
<p>In this chart we see 4 groups of classes: <em>Text Analytics</em>, which is relatively unrelated to interest in any other particular class; a group for “<em>core UX</em>” topics (metrics, surveys, and interviewing); a group for “<em>statistics</em>” (R, Bayes, etc.); and a group for more “<em>marketing</em>” type classes (pricing, choice models, segmentation).</p>
<p>All of that makes complete sense!</p>
<p>I use clustered correlation plots routinely. They are extremely helpful and are a fast way to identify patterns of interest. I often add the correlation coefficients (but rarely show those to stakeholders!) <code>corrplot</code> can do that:</p>
<pre><code class="lang-r">corrplot.mixed(md.cor, upper = <span class="hljs-string">"ellipse"</span>,
               order = <span class="hljs-string">"hclust"</span>, 
               tl.pos = <span class="hljs-string">"lt"</span>, number.cex = <span class="hljs-number">0.75</span>)
</code></pre>
<p>This uses the <code>corrplot.mixed()</code> function to visualize the correlations as ellipses and also show their values. The final options tell it put labels on the left and top (<code>tl.pos=”lt”</code>) and shrink the coefficient text to fit better (<code>number.cex=0.75</code>).</p>
<p>Here’s the result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729298211386/ef37627b-dfc1-4ca7-a2be-f650cc699f77.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-statistically-significant-dont-ask">Statistically Significant? Don’t Ask!</h3>
<p>Because the chart above shows correlation coefficients (Pearson’s <em>r</em> coefficient, as noted above), you might wonder, “are they statistically significant?”</p>
<p><strong>My recommendation is to avoid the questions of “significance”.</strong> We might discuss that theoretically, which is a long discussion — see the topic as discussed in any of my books listed above. However, I will give a few short answers here … plus a visual demonstration.</p>
<p><strong>First</strong>, the correlations here are <em>post-hoc</em> examinations of many coefficients. They do not come from any specific hypothesis test, and thus the usual assumptions of significance testing do not apply. <strong>Second</strong>, in practice we care much more about the absolute <em>strength</em> of association rather than the binary question of “significance”. <strong>Third</strong>, with enough sample, <em>almost everything</em> will be “significant” … and that doesn’t help us decide anything. I’ll say more about that in a moment.</p>
<p><strong>Fourth</strong>, and most importantly, significance testing compares associations in the data to a null hypothesis of “no association” … and <strong>the idea of “no association” is a completely unreasonable hypothesis for such data</strong>. How could we possibly think — or be interested to test — that interest in one class by default is completely unrelated to interest in another class? That notion is easily dispelled simply by talking with people who take classes … or, as we’ll see in moment, by data!</p>
<p>You might be thinking, “show me the code!” Well, here it is!</p>
<p>We can test the association between a single pair of items with the <code>cor.test()</code> function. For example, the association between <em>Text Analytics</em> and <em>Metrics Sprints</em> is one of the smaller <em>r</em> values in the chart above. We can test it with:</p>
<pre><code class="lang-r"><span class="hljs-comment"># correlation testing (not recommended in this case, just demonstrating)</span>
cor.test(md.dat[, <span class="hljs-string">"Text Analytics"</span>], 
         md.dat[, <span class="hljs-string">"Metrics Sprints"</span>])
</code></pre>
<p>R tells us that the Pearson’s <em>r</em> value is <code>0.1234</code> with a CI of <code>(0.0119, 0.2320)</code> … which is “significant” under classical assumptions (or mis-assumptions, as I mention above).</p>
<p>That’s just one pair of items, among the 14 * 13 / 2 = 91 total pairs. How many of those 91 are significant? We do a pairwise test for all pairs with <code>cor.mtest()</code> (“<em>m</em>” for “<em>matrix</em>” or “<em>multiple</em>” as you prefer). Then we compare the p-value to the alpha level of interest — let’s say 0.05 — and find the proportion (using <code>mean()</code> on the boolean comparison) that are “significant”. That’s easier than it might sound:</p>
<pre><code class="lang-r"><span class="hljs-comment"># first get the CIs for each pair</span>
ptest = cor.mtest(md.dat[ , classCols], conf.level = <span class="hljs-number">0.95</span>)

<span class="hljs-comment"># what proportion of those are "significant" ?</span>
mean(ptest$p[upper.tri(ptest$p)] &lt; <span class="hljs-number">0.05</span>)
</code></pre>
<p><strong>Code details</strong>. The second line there should be unpacked slightly. First, we want to compare the p-values (saved in the object <code>ptest</code> as <code>ptest$p</code>) to a threshold value for “significance”, in this case <code>0.05</code>. After making those comparisons, we take the <code>mean()</code> in order to find often that comparison is <code>TRUE</code>. However, the <code>ptest</code> object includes <em>all</em> pairs of comparisons, including comparison of each item with <em>itself</em>, which always has correlation of <em>r</em>\=1.0. In order to exclude those comparison, we select only the “upper triangle” of the matrix of comparisons, indexing the results with <code>upper.tri()</code>.</p>
<p><em>The answer</em>: 71% of all the associations in these data are significant.</p>
<p><strong>One might think, “wow, that’s great!” … but it’s not</strong>. First of all, it merely says that people who are interested in some classes tend to be systematically interested or disinterested in other, related classes. That is not much of a discovery!</p>
<p>Second, it’s not great because — when treated as a binary, “significant” or not — <em>it makes the situation less actionable in practice</em>, rather than <em>more</em> actionable.</p>
<p>To see why the binary question of significance leads to less actionable insight, we can visualize what’s happening. That’s next.</p>
<hr />
<h3 id="heading-visualizing-the-number-of-significant-pairs">Visualizing the Number of “Significant” Pairs</h3>
<p>With <code>corrplot</code>, we can add annotation to charts based on on statistical significance (or, in fact, any matrix that has the same dimensions as the correlation matrix). The steps to do that are (1) to get a matrix with the p-values, as we already did above (<code>ptest</code>), and (2) add that to the correlation plot, indicating how to annotate it.</p>
<p>Given that we already have the matrix <code>ptest</code> with results of significance testing, we can annotate the correlation plot like this:</p>
<pre><code class="lang-r">corrplot(md.cor, method = <span class="hljs-string">'ellipse'</span>, 
         order = <span class="hljs-string">"hclust"</span>, addrect = <span class="hljs-number">4</span>, 
         p.mat = ptest$p,
         sig.level = c(<span class="hljs-number">0.001</span>, <span class="hljs-number">0.01</span>, <span class="hljs-number">0.05</span>), pch.cex = <span class="hljs-number">0.8</span>,
         insig = <span class="hljs-string">"label_sig"</span>)
</code></pre>
<p>The last 3 lines are new here. First, we use <code>p.mat=ptest$p</code> to point to the p-values of interest. Next we set cutoff points for annotating the p-values, where <code>sig.level=c(0.001, 0.01, 0.05)</code> says to use 3 asterisks for p&lt;.001, 2 asterisks for p&lt;.01, and 1 asterisk for p&lt;.05. You could change those cutoffs. <code>pch.cex=0.8</code> shrinks the asterisks to fit better in the cells. Finally <code>insig="label_sig"</code> plots increasing levels (as set by sig.level), as opposed to a single binary flag for significance.</p>
<p>Here’s the result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729359035028/71fd04ef-b3ad-430d-8241-b6e813e50fc0.png" alt class="image--center mx-auto" /></p>
<p>The key takeaway of this chart — compared to the ones above — is that <strong>almost everything is “significant”</strong> (under the [mis-]conception of significance discussed above). There are a few exceptions, especially for the Pricing class, but the general pattern is this: if we know that someone is interested or disinterested in one class, we can make pretty good guesses about what other classes might interest them.</p>
<p>But that is not a surprise! Of course we expect people to share interest — and knowing that their interests go together “significantly” adds nothing actionable to our understanding.</p>
<p>Put differently, <strong>when more than 70% of the boxes on the chart are “significant", there’s nothing in particular we can do with that information</strong>.</p>
<hr />
<h3 id="heading-instead-of-significance">Instead of “Significance”</h3>
<p>So what should we do instead?</p>
<p>First, if a stakeholder answers whether your results are statistically significant, the answer is NOT to dive into any discussion, and certainly not into any coefficients, p-values, or charts with asterisks. Here’s how I recommend to answer:</p>
<blockquote>
<p>Q: Is this statistically significant?</p>
<p>A: Yes! And the actions we should take are …</p>
</blockquote>
<p>How do we arrive at the actions to take? Consider evidence such as:</p>
<ol>
<li><p><strong>What are the overall patterns</strong>? You might, for example, consider the clusters of classes that we saw above.</p>
</li>
<li><p><strong>How strong are the patterns</strong>? We could consider the strength of association — not just whether it is “significant” but how strong it is. Any association above <em>r=0.5</em> or thereabouts is a strong relationship! (For more, try a search for “<em>cohen correlation analysis pearson r strength</em>”. Or see “Learning More” below.)</p>
</li>
<li><p><strong>Are they actionable</strong>? This relates to our business goal. For example, consider in the results here, that Segmentation is closely associated with both Text Analytics and Logs Analysis. Now, suppose we’re going to teach segmentation — because it was the #1 ask as we saw in the previous blog post #1 — and that it will be offered as a half day class. The results suggest that we might add Text Analytics or Logs Analysis as a second half day, in order to maximize appeal to the students taking Segmentation. Or instead — depending on our strategy — it might suggest <em>not</em> teaching one of those, if we want to reach different sets of students between the two half-day sessions.</p>
</li>
</ol>
<p><strong>The key point is this</strong>: is you have enough data to feel good about your sample, and it is quality data, then <em>you generally should not worry about “significance”</em>. Worry instead about how to make <strong>strategic recommendations</strong> and how to help your stakeholders focus on those!</p>
<hr />
<h3 id="heading-final-note-more-on-the-item-clusters">Final Note: More on the Item Clusters</h3>
<p>There is one thing I should emphasize about the clusters here. They are clusters of <em>items</em> — which classes go together in their patterns of interest. They are not clusters of <em>respondents</em>!</p>
<p>Thus, for example, although the patterns clearly identify a “core UX” group of classes (among others), it doesn’t tell us how many people would be <em>interested</em> in that group, nor how strongly they would be interested (or disinterested).</p>
<p>To examine whether there are groups of people who share interest patterns, we need to look at clustering (segmenting) the <em>respondents</em>. That will be the topic of Post #3 in this series — stay tuned!</p>
<hr />
<h3 id="heading-learning-more">Learning More</h3>
<p>As mentioned, stay tuned for Post #3 that will examine clusters of <em>people</em> (i.e., segmentation). That’s a different question than the clusters of <em>items</em> that we considered here.</p>
<p>To learn more about correlation analysis and related methods, check out Chapters 4, 11, and 12 in the <a target="_blank" href="https://www.amazon.com/Marketing-Research-Analytics-Use/dp/3030143155">R book</a>, and Chapters 4, 9, and 10 in the <a target="_blank" href="https://www.amazon.com/Python-Marketing-Research-Analytics-Schwarz/dp/3030497194">Python book</a>. A canonical, classic text for such analyses is Cohen, et al, <em>Applied Multiple Regression / Correlation Analysis for the Behavioral Sciences</em> (<a target="_blank" href="https://www.taylorfrancis.com/books/mono/10.4324/9780203774441/applied-multiple-regression-correlation-analysis-behavioral-sciences-jacob-cohen-patricia-cohen-stephen-west-leona-aiken">link</a>).</p>
<p>And if you’d like to develop hands-on skills in Quant methods — including choice modeling surveys like the one that yielded today’s data — check out the <a target="_blank" href="https://quantuxcon.org/classes">classes from the Quant UX Association</a> (full disclosure, I teach some of them but there are several other instructors lined up, too!)</p>
<hr />
<h3 id="heading-all-the-r-code">All the R Code</h3>
<p>As always, I compile the complete R code in one place. Here it is:</p>
<pre><code class="lang-r"><span class="hljs-comment"># get the data; repeating here for blog post 2, see post 1 for details</span>
<span class="hljs-keyword">library</span>(openxlsx)
md.dat &lt;- read.xlsx(<span class="hljs-string">"https://quantuxbook.com/misc/QUX%20Survey%202024%20-%20Future%20Classes%20-%20MaxDiff%20Individual%20raw%20scores.xlsx"</span>)   <span class="hljs-comment"># </span>
md.dat$Anchor &lt;- <span class="hljs-literal">NULL</span>
names(md.dat)[<span class="hljs-number">3</span>:<span class="hljs-number">16</span>] &lt;- c(<span class="hljs-string">"Choice Models"</span>,  <span class="hljs-string">"Surveys"</span>,       <span class="hljs-string">"Log Sequences"</span>,    <span class="hljs-string">"Psychometrics"</span>, 
                         <span class="hljs-string">"R Programming"</span>,  <span class="hljs-string">"Pricing"</span>,       <span class="hljs-string">"UX Metrics"</span>,       <span class="hljs-string">"Bayes Stats"</span>,
                         <span class="hljs-string">"Text Analytics"</span>, <span class="hljs-string">"Causal Models"</span>, <span class="hljs-string">"Interviewer-ing"</span>,  <span class="hljs-string">"Advanced Choice"</span>, 
                         <span class="hljs-string">"Segmentation"</span>,   <span class="hljs-string">"Metrics Sprints"</span>)
classCols &lt;- <span class="hljs-number">3</span>:ncol(md.dat)    <span class="hljs-comment"># generally, Sawtooth exported utilities start in column 3</span>

<span class="hljs-comment">##### 4. Correlation</span>
md.cor &lt;- cor(md.dat[ , classCols])

<span class="hljs-comment"># basic plot</span>
<span class="hljs-keyword">library</span>(corrplot)
corrplot(md.cor)

<span class="hljs-comment"># correlations with clustering</span>
corrplot(md.cor, method = <span class="hljs-string">"ellipse"</span>,
         order = <span class="hljs-string">"hclust"</span>, addrect = <span class="hljs-number">4</span>) 

<span class="hljs-comment"># add correlation coefficients</span>
corrplot.mixed(md.cor, upper = <span class="hljs-string">"ellipse"</span>,
               order = <span class="hljs-string">"hclust"</span>, 
               tl.pos = <span class="hljs-string">"lt"</span>, number.cex = <span class="hljs-number">0.75</span>)

<span class="hljs-comment"># correlation testing (not recommended in this case, just demonstrating)</span>
cor.test(md.dat[, <span class="hljs-string">"Text Analytics"</span>], 
         md.dat[, <span class="hljs-string">"Metrics Sprints"</span>])

<span class="hljs-comment"># plot with significance highlighted</span>
<span class="hljs-comment"># first get the CIs for each pair</span>
ptest = cor.mtest(md.dat[ , classCols], conf.level = <span class="hljs-number">0.95</span>)

<span class="hljs-comment"># what proportion of those are "significant" ?</span>
mean(ptest$p &lt; <span class="hljs-number">0.05</span>)

<span class="hljs-comment"># add asterisks to the plot for "significant" at p=0.05 level</span>
corrplot(md.cor, method = <span class="hljs-string">'ellipse'</span>, 
         order = <span class="hljs-string">"hclust"</span>, addrect = <span class="hljs-number">4</span>, 
         p.mat = ptest$p,
         sig.level = c(<span class="hljs-number">0.001</span>, <span class="hljs-number">0.01</span>, <span class="hljs-number">0.05</span>), pch.cex = <span class="hljs-number">0.8</span>,
         insig = <span class="hljs-string">"label_sig"</span>)
</code></pre>
<p><a target="_blank" href="https://notbyai.fyi"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746999945541/9d224843-9e9f-44cc-98c5-276915794420.png" alt class="image--center mx-auto" /></a></p>
]]></content:encoded></item><item><title><![CDATA[Individual Scores in Choice Models, Part 1: Data & Averages]]></title><description><![CDATA[Before jumping into today’s topic, I will highlight the previous post in case you missed it: guest author — and coauthor of the Quant UX book — Kerry Rodden discusses HEART metrics. It updates Kerry’s classic description of HEART with new reflections...]]></description><link>https://quantuxblog.com/individual-scores-in-choice-models-part-1-data-averages</link><guid isPermaLink="true">https://quantuxblog.com/individual-scores-in-choice-models-part-1-data-averages</guid><category><![CDATA[R Language]]></category><category><![CDATA[quantux]]></category><category><![CDATA[uxresearch]]></category><category><![CDATA[marketing research]]></category><category><![CDATA[conjoint]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Wed, 23 Oct 2024 14:33:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/JG35CpZLfVs/upload/3a43345ebb4901b421b9d12f93520998.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><em>Before jumping into today’s topic, I will highlight the</em> <a target="_blank" href="https://quantuxblog.com/how-to-make-heart-metrics-work-in-practice"><em>previous post</em></a> <em>in case you missed it: guest author — and coauthor of the Quant UX book — Kerry Rodden discusses HEART metrics. It updates Kerry’s classic description of HEART with new reflections on how to make UX metrics succeed in practice.</em> <a target="_blank" href="https://quantuxblog.com/how-to-make-heart-metrics-work-in-practice"><em>Read it here</em></a><em>!</em></p>
</blockquote>
<p>Today I begin a series of posts that demonstrate working in R with individual level estimates from choice models such as MaxDiff and Conjoint Analysis. Those are the best estimates of preference for each respondent to a choice survey. I hope they will inspire you both to run choice surveys and to learn more from them!</p>
<hr />
<h3 id="heading-background-and-assumptions">Background and Assumptions</h3>
<p><strong>Assumptions</strong>: I <em>assume</em> that you know what a choice model survey is, and — at a high level — what individual scores are. If not, check out <a target="_blank" href="https://quantuxblog.com/easy-maxdiff-in-r">this post about MaxDiff surveys</a>. Or take one of the upcoming <a target="_blank" href="https://www.quantuxcon.org/classes">Choice Modeling Master Classes</a> from the Quant Association! Or for even more, find sections on MaxDiff in the <a target="_blank" href="https://www.amazon.com/Quantitative-User-Experience-Research-Understanding/dp/1484292677">Quant UX book</a> and discussions of Conjoint Analysis in the <a target="_blank" href="https://www.amazon.com/Marketing-Research-Analytics-Use/dp/3030143155">R book</a> and <a target="_blank" href="https://www.amazon.com/Python-Marketing-Research-Analytics-Schwarz/dp/3030497194">Python book</a>.</p>
<p>However, to recap briefly, individual scores are the best estimates for each respondent who completed a choice survey. For example, they report each respondents’ interest — as estimated by the model — for every one of the items tested in the survey.</p>
<p><strong>Data.</strong> Here I discuss data from N=308 UX Researchers who took a <a target="_blank" href="https://quantuxblog.com/easy-maxdiff-in-r">MaxDiff survey</a> about potential classes from the Quant UX Association. As with most Quant Association surveys, the respondents agreed that we could use the (anonymous) data publicly. Yay! It is great to discuss realistic choice model data. The survey included 14 potential classes. Here is an example MaxDiff screen:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729177927658/0b703d58-8a4c-47a6-8940-e90ff38de22c.png" alt="a MaxDiff screen asking respondents to select the most and least interesting class, from a random subset of 5 classes" class="image--center mx-auto" /></p>
<p><strong>Data Format</strong>. The estimates here were estimated by Sawtooth Software’s Discover product. The code here begins with an Excel file exactly as exported by Discover. Each row is represents one respondent’s interest in each of the potential classes.</p>
<blockquote>
<p>Note: Without going into the details of hierarchical Bayes (HB) models, I’ll note that the HB process gives 1000s of estimates for each respondent. The values here — the ones that most analysts would use — are the mean (average) of those for each person. Although the estimates are somewhat uncertain for one person, the overall <em>set</em> of estimates are an excellent representation for the group.</p>
</blockquote>
<p><strong>R code.</strong> I assume you can generally follow R. A key goal of this post is to share R code that will help you get accelerate your own work. (More in the <a target="_blank" href="https://www.amazon.com/Marketing-Research-Analytics-Use/dp/3030143155">R book</a>!)</p>
<p>You should be able to follow along live in R — all of the data and code are shared here. In each block of R code, you can use the <strong>copy icon</strong> in the upper right to copy the code and paste it into R to follow along. Or see the end of the post for all of the code in one big chunk.</p>
<p><strong>Packages</strong>. You may need to install extra R packages along the way. If you get an error running a “<code>library()</code>” command, you probably need to install that package.</p>
<hr />
<h3 id="heading-load-the-data">Load the Data</h3>
<p>Sawtooth Discover exports individual estimates as Excel (<code>.xlsx</code>) files. I’ve uploaded that file, and can download it directly from its URL using the <code>openxlsx</code> package:</p>
<pre><code class="lang-r"><span class="hljs-comment"># Read Excel data as saved by Sawtooth</span>
<span class="hljs-keyword">library</span>(openxlsx)   <span class="hljs-comment"># install if needed</span>
<span class="hljs-comment"># online location (assuming you are able to read it)</span>
md.dat &lt;- read.xlsx(<span class="hljs-string">"https://quantuxbook.com/misc/QUX%20Survey%202024%20-%20Future%20Classes%20-%20MaxDiff%20Individual%20raw%20scores.xlsx"</span>)   <span class="hljs-comment">#</span>
</code></pre>
<p>It’s helpful to do a bit of minor cleanup. First of all, in Anchored MaxDiff, each item is compared to an “anchor” of whether there is positive interest or not. The anchor estimate is always exported as “0”, so we can remove that as non-informative.</p>
<p>Second, the file sets column (variable) names that are the entire text of each item. It’s helpful to replace those with shorter versions that are easy to read.</p>
<p>Those steps are:</p>
<pre><code class="lang-r"><span class="hljs-comment"># Some minor clean up</span>
<span class="hljs-comment"># remove the "Anchor" item that is always 0 after individual calibration</span>
md.dat$Anchor &lt;- <span class="hljs-literal">NULL</span>
<span class="hljs-comment"># Assign friendly names instead of the long names, so we can plot them better</span>
names(md.dat)[<span class="hljs-number">3</span>:<span class="hljs-number">16</span>]  <span class="hljs-comment"># check these to make sure we're renaming correctly</span>
names(md.dat)[<span class="hljs-number">3</span>:<span class="hljs-number">16</span>] &lt;- c(<span class="hljs-string">"Choice Models"</span>,  <span class="hljs-string">"Surveys"</span>,       <span class="hljs-string">"Log Sequences"</span>,    <span class="hljs-string">"Psychometrics"</span>, 
                         <span class="hljs-string">"R Programming"</span>,  <span class="hljs-string">"Pricing"</span>,       <span class="hljs-string">"UX Metrics"</span>,       <span class="hljs-string">"Bayes Stats"</span>,
                         <span class="hljs-string">"Text Analytics"</span>, <span class="hljs-string">"Causal Models"</span>, <span class="hljs-string">"Interviewer-ing"</span>,  <span class="hljs-string">"Advanced Choice"</span>, 
                         <span class="hljs-string">"Segmentation"</span>,   <span class="hljs-string">"Metrics Sprints"</span>)
</code></pre>
<p>Next, I use <code>str()</code> and <code>summary()</code> to do some basic data checks:</p>
<pre><code class="lang-r"><span class="hljs-comment"># Basic data check</span>
str(md.dat)
summary(md.dat)
</code></pre>
<p>Finally, I set a variable for the item columns (columns <code>3:[end]</code>) so I can refer to them easily, without a magic number “3” showing up multiple times later:</p>
<pre><code class="lang-r"><span class="hljs-comment"># Index the columns for the classes, since we'll use that repeatedly</span>
classCols &lt;- <span class="hljs-number">3</span>:ncol(md.dat)    <span class="hljs-comment"># generally, Sawtooth exported utilities start in column 3</span>
</code></pre>
<p>With those lines, we have data! It’s time to look at the results.</p>
<hr />
<h3 id="heading-initial-chart-overall-estimates">Initial Chart: Overall Estimates</h3>
<p>The first thing we might want to know — or that stakeholders will ask — is which classes are most desired. In the spreadsheet world, that might be done by finding the average level of interest and then creating a bar chart. We’ll do the same thing here: find the average interest and then generate a bar chart.</p>
<p>In R, there are many different ways to obtain column averages. I’ll use <code>lapply( , mean)</code> … just because it occurred to me first. I use <code>barplot()</code> to plot them:</p>
<pre><code class="lang-r"><span class="hljs-comment"># First, get the average values for the classes</span>
mean.dat &lt;- lapply(md.dat[ , classCols], mean)

<span class="hljs-comment"># Plot a simple bar chart of those averages</span>
<span class="hljs-comment"># first set the chart borders so everything will fit [this is trial and error]</span>
par(mar = c(<span class="hljs-number">3</span>, <span class="hljs-number">8</span>, <span class="hljs-number">2</span>, <span class="hljs-number">2</span>))
<span class="hljs-comment"># then draw the chart</span>
barplot(height = unlist(mean.dat), names.arg = names(mean.dat),    <span class="hljs-comment"># column values and labels</span>
        horiz = <span class="hljs-literal">TRUE</span>, col =<span class="hljs-string">"darkblue"</span>,                           <span class="hljs-comment"># make it horizontal and blue</span>
        cex.names = <span class="hljs-number">0.75</span>, las = <span class="hljs-number">1</span>,                                   <span class="hljs-comment"># shrink the labels and rotate them</span>
        main = <span class="hljs-string">"Interest Level by Quant Course"</span>)
</code></pre>
<p>In the plot, I set margins for the graphics window with <code>par(mar=…)</code> to make it fit. That is a trial and error process for base R plots. I set the <code>barplot()</code> to be <code>horiz</code>[ontal] and slightly shrink (<code>cex.names=0.75</code>) and rotate the labels (<code>las=1</code>) to be readable.</p>
<p>Here’s the result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729178394515/09ee901b-6dc8-4aec-8e05-7252a3c7c02e.png" alt class="image--center mx-auto" /></p>
<p>It is very, well, spreadsheet-ish. We see the winner (segmentation) and loser (interviewer training) … but we can do much better!</p>
<hr />
<h3 id="heading-better-chart-estimates-with-error-bars">Better Chart: Estimates with Error Bars</h3>
<p>In the chart above, we can see the averages but have no insight into the distribution. Are the averages strongly different? Or are they very close in comparison to the underlying distributions? Put differently, is the “winner” (segmentation) really much stronger than the next three options (psychometrics, etc.) or is it only slightly better?</p>
<p>A better chart would show error bars for the means, so that we can tell whether differences are — to use the common but somewhat misleading term (for reasons I’ll set aside) — “significant”. We’ll do that by using the <code>geom_errorbar()</code> visualization option in <code>ggplot2</code>.</p>
<p>I do lots of choice models and have to make plots like this all the time. I often make similar plots repeatedly, plotting different subsets of data, different samples, and the like. For that, it is useful to make a <strong><em>function</em></strong> for such plots. I can simply reuse one function and not rely on error-prone copying and repetition of code.</p>
<p>Here’s a relatively basic function to plot average estimates from an anchored MaxDiff with error bars. (For other estimates such as general MaxDiff or conjoint, it will also work; just change the <code>xlab()</code> label, either in the function or by adding it afterward.)</p>
<pre><code class="lang-r">plot.md.mean &lt;- <span class="hljs-keyword">function</span>(dat, itemCols) {
  <span class="hljs-keyword">library</span>(ggplot2)
  <span class="hljs-keyword">library</span>(reshape2)
  <span class="hljs-comment"># warning, next line assumes we're using Sawtooth formatted data!</span>
  md.m &lt;- melt(dat[ , c(<span class="hljs-number">1</span>, itemCols)])   <span class="hljs-comment"># add column 1 for the respondent ID</span>
  <span class="hljs-comment"># put them in mean order</span>
  <span class="hljs-keyword">library</span>(forcats)
  md.m$variable &lt;- fct_reorder(md.m$variable, md.m$value, .fun = mean)

  p &lt;- ggplot(data = md.m, aes(x = value, y = variable)) +
    <span class="hljs-comment"># error bars according to bootstrap estimation ("width" is of the lines, not the CIs)</span>
    geom_errorbar(stat = <span class="hljs-string">"summary"</span>, fun.data = mean_cl_boot, width = <span class="hljs-number">0.4</span>,) +
    <span class="hljs-comment"># add points for the mean value estimates</span>
    geom_point(size = <span class="hljs-number">4</span>, stat = <span class="hljs-string">"summary"</span>, fun = mean, shape = <span class="hljs-number">20</span>) +
    <span class="hljs-comment"># clean up the chart</span>
    theme_minimal() +
    xlab(<span class="hljs-string">"Average interest &amp; CI (0=Anchor)"</span>) +
    ylab(<span class="hljs-string">"Quant Course"</span>) 

  p 
}
</code></pre>
<p>There’s not a lot to say about this function. First, it melts the data to fit typical <code>ggplot</code> patterns. It adds the identifier column (<code>1</code>) for <code>melt()</code>; that would need adjusting if you have differently formatted data. Then it calls <code>fct_reorder()</code> from the <code>forcats</code> library to put the labels into order — in this case, to order them by the <code>mean</code> value of the grouped data. The error bars are plotted by <code>geom_errorbar()</code>, and that uses the <code>mean_cl_boot</code> option to find the confidence intervals by bootstrapping. (That function is in <code>Hmisc</code>, another potential package to install). Finally, after plotting the error bars, it adds the actual mean points with <code>geom_point()</code>.</p>
<p>Now that we have a function, it is a simple command to plot the data:</p>
<pre><code class="lang-r">plot.md.mean(md.dat, classCols)
</code></pre>
<p>Here’s the result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729195497375/18c75c95-e6dc-4135-9784-64ff64c308fe.png" alt class="image--center mx-auto" /></p>
<p>Now we can see that the segmentation class is a fairly strong #1, while the next 3 classes (psychometrics, choice, surveys) are essentially tied. Among the 14 classes, 13 have average interest greater than zero — the MaxDiff anchor — while interviewer training falls below the anchor on average.</p>
<p>As a final note, because the function returns a <code>ggplot</code> object “<code>p</code>”, we could add other <code>ggplot2</code> options. For instance, we might add “<code>+ ggtitle(“My title!”)</code>” to add a title or change the y axis label with “<code>+ ylab(“a different label”)</code>”</p>
<p>The <strong>drawback</strong> of this plot is the following: it assumes that we want to know how classes compare in <em>average</em> values, according to <em>statistical significance</em>. In actual practice, that is not usually the case.</p>
<p>Why not? We are usually uninterested in averages and their confidence intervals? Because most often, practitioners need to know <em>how many</em> respondents are interested in something, and <em>how many</em> of them are <em>strongly interested</em> or <em>disinterested</em>. We do not reach any “average” customer — we reach individuals.</p>
<p>So although an average chart with error bars is a good high-level view, there is more to learn.That brings us to my favorite chart: individual distributions!</p>
<hr />
<h3 id="heading-another-great-chart-individual-distributions">Another Great Chart: Individual Distributions</h3>
<p>There is one important question that an average chart — such as the ones above — cannot answer. That question is: “<em>OK, this is the best item on average … but which item has people who are the</em> <strong>very most</strong> <em>interested in it?</em>”</p>
<p>For that, it is helpful to examine the individual distributions — not only where respondents are on average but whether there are groups who differ strongly in interest above or below the average.</p>
<p>As you probably guessed, I’ll plot it with a reusable function! Here’s the function. It’s long but I’ll break it down below.</p>
<pre><code class="lang-r">cbc.plot &lt;- <span class="hljs-keyword">function</span>(dat, itemCols = <span class="hljs-number">3</span>:ncol(dat), 
                     title = <span class="hljs-string">"Preference estimates: Overall + Individual level"</span>, 
                     meanOrder = <span class="hljs-literal">TRUE</span>) {

    <span class="hljs-comment"># get the mean points so we can plot those over the density plot</span>
  mean.df &lt;- lapply(dat[ , itemCols], mean)

  <span class="hljs-comment"># melt the data for ggplot</span>
  <span class="hljs-keyword">library</span>(reshape2)
  <span class="hljs-comment">#                       vvvv  assumes Sawtooth order;       vvv  (ID in col 1, remove RLH in col 2)</span>
  plot.df &lt;- melt(dat[, c(<span class="hljs-number">1</span>, itemCols)], id.vars = names(dat)[<span class="hljs-number">1</span>])

  <span class="hljs-comment"># get the N of respondents so we can set an appropriate level of point transparency</span>
  p.resp  &lt;- length(unique(plot.df[ , <span class="hljs-number">1</span>]))

  <span class="hljs-comment"># optionally and by default order the results not by column but by mean value</span>
  <span class="hljs-comment"># because ggplot builds from the bottom, we'll reverse them to put max value at the top</span>
  <span class="hljs-comment"># we could use fct_reorder but manually setting the order is straightforward in this case</span>
  <span class="hljs-keyword">if</span> (meanOrder) {
    plot.df$variable &lt;- factor(plot.df$variable, levels = rev(names(mean.df)[order(unlist(mean.df))]))
  }

  <span class="hljs-comment">#### Now : Build the plot</span>
  <span class="hljs-comment"># set.seed(ran.seed)   # optional; points are jittered; setting a seed would make them exactly reproducible</span>
  <span class="hljs-keyword">library</span>(ggplot2)
  <span class="hljs-keyword">library</span>(ggridges)

  <span class="hljs-comment"># build the first layer with the individual distributions</span>
  p &lt;- ggplot(data=plot.df, aes(x=value, y=variable, group=variable)) +
    geom_density_ridges(scale=<span class="hljs-number">0.9</span>, alpha=<span class="hljs-number">0</span>, jittered_points=<span class="hljs-literal">TRUE</span>,
                        rel_min_height=<span class="hljs-number">0.005</span>,
                        position=<span class="hljs-string">"points_sina"</span>,
                        <span class="hljs-comment"># set individual point alphas in inverse proportion to sample size</span>
                        point_color=<span class="hljs-string">"blue"</span>, point_alpha=<span class="hljs-number">1</span>/sqrt(p.resp),
                        point_size=<span class="hljs-number">2.5</span>) +
    <span class="hljs-comment"># reverse y axis to match attribute order from top</span>
    scale_y_discrete(limits=rev) +                                                   
    ylab(<span class="hljs-string">"Level"</span>) + xlab(<span class="hljs-string">"Relative preference (blue=individuals, red=average)"</span>) +
    ggtitle(title) +
    theme_minimal()

  <span class="hljs-comment"># now add second layer to plot with the means of each item distribution</span>
  <span class="hljs-keyword">for</span> (i <span class="hljs-keyword">in</span> <span class="hljs-number">1</span>:length(mean.df)) {
    <span class="hljs-keyword">if</span> (meanOrder) {
      <span class="hljs-comment"># if we're drawing them in mean order, get the right one same as above</span>
      p &lt;- p + geom_point(x=mean.df[[rev(order(unlist(mean.df)))[i]]], 
                          y=length(mean.df)-i+<span class="hljs-number">1</span>, colour=<span class="hljs-string">"tomato"</span>,      <span class="hljs-comment"># adjust y axis because axis is reversed above</span>
                          alpha=<span class="hljs-number">0.5</span>, size=<span class="hljs-number">2.0</span>, shape=<span class="hljs-number">0</span>, inherit.aes=<span class="hljs-literal">FALSE</span>)

    } <span class="hljs-keyword">else</span> {
      p &lt;- p + geom_point(x=mean.df[[i]], 
                          y=length(mean.df)-i+<span class="hljs-number">1</span>, colour=<span class="hljs-string">"tomato"</span>,      <span class="hljs-comment"># adjust y axis because axis is reversed above</span>
                          alpha=<span class="hljs-number">0.5</span>, size=<span class="hljs-number">2.0</span>, shape=<span class="hljs-number">0</span>, inherit.aes=<span class="hljs-literal">FALSE</span>)
    }
  }
  p
}
</code></pre>
<p>In the first couple of lines of the function, it finds the average value for each item using <code>lapply()</code>, the same as we already saw above. That’s so we can add those as a separate layer on the plot later. Then it <code>melts</code> the data, again just like saw above.</p>
<p>Next, it finds the total N of respondents and saves that as <code>p.resp</code>. Why? Because when we plot the individuals, we want to set a transparency <code>alpha</code> value. Setting <code>alpha</code> in inverse proportion to the (<em>square root of the</em>) number of respondents makes those points more legible.</p>
<p>By default, it puts the labels into their mean order, using the averages we calculated instead of <code>fct_reorder()</code> as above (everything in R has multiple good options!)</p>
<p>The next two big chunks build the plot in two states. The first big chunk uses the <code>ggridges</code> package to plot <code>geom_density_ridges()</code> density curves for the individual distributions. I won’t try to explain those; just look at the chart below! Its options add individual points to the curves, and sets a transparency alpha as I described above.</p>
<p>The second big chunk adds points to the chart, overlaying the density ridges with the average values for each item. To do that, it iterates over the items with a <code>for()</code> loop, and then adds the point in the proper place according to whether the items are displayed in sorted order or not.</p>
<p>We call the plot with a simple command, adding a custom y axis label:</p>
<pre><code class="lang-r">cbc.plot(md.dat, itemCols=classCols) + ylab(<span class="hljs-string">"Quant Course Offering"</span>)
</code></pre>
<p>Here’s the result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729197094156/af9ff8be-1f77-42c3-9101-d1fa11066cc9.png" alt class="image--center mx-auto" /></p>
<p>Wow! This chart has a lot of great information.</p>
<p>I won’t interpret it in complete detail but will note a couple of interesting features. First, it reinforces that Segmentation is a strong #1 option — not only does it have the highest average, more than 90% of respondents show positive interest greater than the anchor value of 0. We also see at the upper end of interest — the right hand side — that Segmentation has many more respondents with particularly strong interest (greater than a value of 5.0, to choose an arbitrary point) than any other class.</p>
<p>However, we see some other things with subsets of respondents with high interest. For example, although the R Programming course is a weak #11 out of 14 in <em>average</em> interest, it has a small number of respondents showing the <em>strongest</em> interest of anyone in any class.</p>
<p>When we consider that hands-on classes are small, and reach only the people interested in them, these results suggest that an R class could be an good offering, even if the average is lower. We don’t care how people are <em>uninterested</em> — we only care whether we can reach enough people who are interested!</p>
<p>With that, I’ll leave you to inspect the chart and find other interesting ideas.</p>
<hr />
<h3 id="heading-coming-up-in-post-2">Coming up in Post 2</h3>
<p>In the next post, I’ll go farther with these data and examine:</p>
<ul>
<li><p><a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-2-correlations-among-items"><strong>Correlations among interests</strong></a>: if they like X, what else do they like or dislike?</p>
</li>
<li><p><strong>Finding clusters of classes</strong> that go together (item clusters — we’ll look at respondent clusters in post 3)</p>
</li>
<li><p>… and later posts will look at <strong>respondent segmentation</strong> and (briefly) <strong>data quality</strong></p>
</li>
</ul>
<p><a target="_blank" href="https://quantuxblog.com/individual-scores-in-choice-models-part-2-correlations-among-items">Stay tuned for Post #2</a> in a few days!</p>
<p>Meanwhile, if you’re interested in more about choice models and/or R, check out <a target="_blank" href="https://quantuxblog.com/easy-maxdiff-in-r">this post about MaxDiff surveys</a>; and upcoming <a target="_blank" href="https://www.quantuxcon.org/classes">Choice Modeling Master Classes</a> from the Quant UX Association; and sections on MaxDiff in the <a target="_blank" href="https://www.amazon.com/Quantitative-User-Experience-Research-Understanding/dp/1484292677">Quant UX book</a> and Conjoint Analysis in the <a target="_blank" href="https://www.amazon.com/Marketing-Research-Analytics-Use/dp/3030143155">R book</a> or <a target="_blank" href="https://www.amazon.com/Python-Marketing-Research-Analytics-Schwarz/dp/3030497194">Python book</a>. Also, for more experienced choice modelers, I recently shared this <a target="_blank" href="https://quantuxblog.com/misconceptions-about-conjoint-analysis">post about misunderstandings of Conjoint Analysis</a>.</p>
<hr />
<h3 id="heading-all-the-code">All the Code</h3>
<p>As promised, following is all of the R code from this post. You can use the copy icon in the upper right to grab it all at once, and paste into RStudio or wherever you code.</p>
<p>Cheers!</p>
<pre><code class="lang-r"><span class="hljs-comment"># blog scripts for analysis of individual level MaxDiff data</span>
<span class="hljs-comment"># Chris Chapman, October 2024</span>

<span class="hljs-comment">##### 1. Get individual-level mean beta estimates as exported by Sawtooth Software</span>

<span class="hljs-comment"># 1a. read Excel data as saved by Sawtooth</span>
<span class="hljs-keyword">library</span>(openxlsx)
<span class="hljs-comment"># online location (assuming you are able to read it)</span>
md.dat &lt;- read.xlsx(<span class="hljs-string">"https://quantuxbook.com/misc/QUX%20Survey%202024%20-%20Future%20Classes%20-%20MaxDiff%20Individual%20raw%20scores.xlsx"</span>)   <span class="hljs-comment"># </span>

<span class="hljs-comment"># 1b. Some minor clean up</span>
<span class="hljs-comment"># remove the "Anchor" item that is always 0 after individual calibration</span>
md.dat$Anchor &lt;- <span class="hljs-literal">NULL</span>
<span class="hljs-comment"># Assign friendly names instead of the long names, so we can plot them better</span>
names(md.dat)[<span class="hljs-number">3</span>:<span class="hljs-number">16</span>]  <span class="hljs-comment"># check these to make sure we're renaming correctly</span>
names(md.dat)[<span class="hljs-number">3</span>:<span class="hljs-number">16</span>] &lt;- c(<span class="hljs-string">"Choice Models"</span>,  <span class="hljs-string">"Surveys"</span>,       <span class="hljs-string">"Log Sequences"</span>,    <span class="hljs-string">"Psychometrics"</span>, 
                         <span class="hljs-string">"R Programming"</span>,  <span class="hljs-string">"Pricing"</span>,       <span class="hljs-string">"UX Metrics"</span>,       <span class="hljs-string">"Bayes Stats"</span>,
                         <span class="hljs-string">"Text Analytics"</span>, <span class="hljs-string">"Causal Models"</span>, <span class="hljs-string">"Interviewer-ing"</span>,  <span class="hljs-string">"Advanced Choice"</span>, 
                         <span class="hljs-string">"Segmentation"</span>,   <span class="hljs-string">"Metrics Sprints"</span>)

<span class="hljs-comment"># 1c. Basic data check</span>
str(md.dat)
summary(md.dat)

<span class="hljs-comment"># 1d. Index the columns for the classes, since we'll use that repeatedly</span>
classCols &lt;- <span class="hljs-number">3</span>:ncol(md.dat)    <span class="hljs-comment"># generally, Sawtooth exported utilities start in column 3</span>


<span class="hljs-comment">##### 2. Plot the overall means</span>

<span class="hljs-comment"># 2a. The easy way (but not so good)</span>
<span class="hljs-comment"># First, get the average values for the classes</span>
mean.dat &lt;- lapply(md.dat[ , classCols], mean)

<span class="hljs-comment"># Plot a simple bar chart of those averages</span>
<span class="hljs-comment"># first set the chart borders so everything will fit [this is trial and error]</span>
par(mar=c(<span class="hljs-number">3</span>, <span class="hljs-number">8</span>, <span class="hljs-number">2</span>, <span class="hljs-number">2</span>))
<span class="hljs-comment"># then draw the chart</span>
barplot(height=unlist(mean.dat), names.arg = names(mean.dat),    <span class="hljs-comment"># column values and labels</span>
        horiz = <span class="hljs-literal">TRUE</span>, col =<span class="hljs-string">"darkblue"</span>,                           <span class="hljs-comment"># make it horizontal and blue</span>
        cex.names=<span class="hljs-number">0.75</span>, las=<span class="hljs-number">1</span>,                                   <span class="hljs-comment"># shrink the labels and rotate them</span>
        main =<span class="hljs-string">"Interest Level by Quant Course"</span>)


<span class="hljs-comment"># 2b. A somewhat more complex (but much better) way</span>
<span class="hljs-comment">#     we'll make this a function ... it's often good to make anything long into a function :)</span>
plot.md.mean &lt;- <span class="hljs-keyword">function</span>(dat, itemCols) {
  <span class="hljs-keyword">library</span>(ggplot2)
  <span class="hljs-keyword">library</span>(reshape2)
  <span class="hljs-comment"># warning, next line assumes we're using Sawtooth formatted data!</span>
  md.m &lt;- melt(dat[ , c(<span class="hljs-number">1</span>, itemCols)])   <span class="hljs-comment"># add column 1 for the respondent ID</span>
  <span class="hljs-comment"># put them in mean order</span>
  <span class="hljs-keyword">library</span>(forcats)
  md.m$variable &lt;- fct_reorder(md.m$variable, md.m$value, .fun=mean)

  p &lt;- ggplot(data=md.m, aes(x=value, y=variable)) +
    <span class="hljs-comment"># error bars according to bootstrap estimation ("width" is of the lines, not the CIs)</span>
    geom_errorbar(stat = <span class="hljs-string">"summary"</span>, fun.data = mean_cl_boot, width = <span class="hljs-number">0.4</span>,) +
    <span class="hljs-comment"># add points for the mean value estimates</span>
    geom_point(size = <span class="hljs-number">4</span>, stat = <span class="hljs-string">"summary"</span>, fun = mean, shape = <span class="hljs-number">20</span>) +
    <span class="hljs-comment"># clean up the chart</span>
    theme_minimal() +
    xlab(<span class="hljs-string">"Average interest &amp; CI (0=Anchor)"</span>) +
    ylab(<span class="hljs-string">"Quant Course"</span>) 

  p 
}
<span class="hljs-comment"># call our plot</span>
plot.md.mean(md.dat, classCols)



<span class="hljs-comment">##### 3. Even better: Plot the distributions (individual estimates)</span>

cbc.plot &lt;- <span class="hljs-keyword">function</span>(dat, itemCols=<span class="hljs-number">3</span>:ncol(dat), 
                     title = <span class="hljs-string">"Preference estimates: Overall + Individual level"</span>, 
                     meanOrder=<span class="hljs-literal">TRUE</span>) {

    <span class="hljs-comment"># get the mean points so we can plot those over the density plot</span>
  mean.df &lt;- lapply(dat[ , itemCols], mean)

  <span class="hljs-comment"># melt the data for ggplot</span>
  <span class="hljs-keyword">library</span>(reshape2)
  <span class="hljs-comment">#                       vvvv  assumes Sawtooth order;       vvv  (ID in col 1, remove RLH in col 2)</span>
  plot.df &lt;- melt(dat[, c(<span class="hljs-number">1</span>, itemCols)], id.vars=names(dat)[<span class="hljs-number">1</span>])

  <span class="hljs-comment"># get the N of respondents so we can set an appropriate level of point transparency</span>
  p.resp  &lt;- length(unique(plot.df[ , <span class="hljs-number">1</span>]))

  <span class="hljs-comment"># optionally and by default order the results not by column but by mean value</span>
  <span class="hljs-comment"># because ggplot builds from the bottom, we'll reverse them to put max value at the top</span>
  <span class="hljs-comment"># we could use fct_reorder but manually setting the order is straightforward in this case</span>
  <span class="hljs-keyword">if</span> (meanOrder) {
    plot.df$variable &lt;- factor(plot.df$variable, levels = rev(names(mean.df)[order(unlist(mean.df))]))
  }

  <span class="hljs-comment">#### Now : Build the plot</span>
  <span class="hljs-comment"># set.seed(ran.seed)   # optional; points are jittered; setting a seed would make them exactly reproducible</span>
  <span class="hljs-keyword">library</span>(ggplot2)
  <span class="hljs-keyword">library</span>(ggridges)

  <span class="hljs-comment"># build the first layer with the individual distributions</span>
  p &lt;- ggplot(data=plot.df, aes(x=value, y=variable, group=variable)) +
    geom_density_ridges(scale=<span class="hljs-number">0.9</span>, alpha=<span class="hljs-number">0</span>, jittered_points=<span class="hljs-literal">TRUE</span>,
                        rel_min_height=<span class="hljs-number">0.005</span>,
                        position=<span class="hljs-string">"points_sina"</span>,
                        <span class="hljs-comment"># set individual point alphas in inverse proportion to sample size</span>
                        point_color = <span class="hljs-string">"blue"</span>, point_alpha=<span class="hljs-number">1</span>/sqrt(p.resp),
                        point_size=<span class="hljs-number">2.5</span>) +
    <span class="hljs-comment"># reverse y axis to match attribute order from top</span>
    scale_y_discrete(limits=rev) +                                                   
    ylab(<span class="hljs-string">"Level"</span>) + xlab(<span class="hljs-string">"Relative preference (blue=individuals, red=average)"</span>) +
    ggtitle(title) +
    theme_minimal()

  <span class="hljs-comment"># now add second layer to plot with the means of each item distribution</span>
  <span class="hljs-keyword">for</span> (i <span class="hljs-keyword">in</span> <span class="hljs-number">1</span>:length(mean.df)) {
    <span class="hljs-keyword">if</span> (meanOrder) {
      <span class="hljs-comment"># if we're drawing them in mean order, get the right one same as above</span>
      p &lt;- p + geom_point(x=mean.df[[rev(order(unlist(mean.df)))[i]]], 
                          y=length(mean.df)-i+<span class="hljs-number">1</span>, colour=<span class="hljs-string">"tomato"</span>,      <span class="hljs-comment"># adjust y axis because axis is reversed above</span>
                          alpha=<span class="hljs-number">0.5</span>, size=<span class="hljs-number">2.0</span>, shape=<span class="hljs-number">0</span>, inherit.aes=<span class="hljs-literal">FALSE</span>)

    } <span class="hljs-keyword">else</span> {
      p &lt;- p + geom_point(x=mean.df[[i]], 
                          y=length(mean.df)-i+<span class="hljs-number">1</span>, colour=<span class="hljs-string">"tomato"</span>,      <span class="hljs-comment"># adjust y axis because axis is reversed above</span>
                        alpha=<span class="hljs-number">0.5</span>, size=<span class="hljs-number">2.0</span>, shape=<span class="hljs-number">0</span>, inherit.aes=<span class="hljs-literal">FALSE</span>)
    }
  }
  p
}

cbc.plot(md.dat, itemCols=classCols) + ylab(<span class="hljs-string">"Quant Course Offering"</span>)
</code></pre>
<p><a target="_blank" href="https://notbyai.fyi"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746999945541/9d224843-9e9f-44cc-98c5-276915794420.png" alt class="image--center mx-auto" /></a></p>
]]></content:encoded></item><item><title><![CDATA[How to make HEART metrics work in practice]]></title><description><![CDATA[👋 I'm happy to be making my first contribution to the Quant UX Blog! I'm Chris's co-author on the Quant UX Research book, and I'm excited to join him in sharing perspectives that complement the book.
I led the early Quant UX Research team at Google,...]]></description><link>https://quantuxblog.com/how-to-make-heart-metrics-work-in-practice</link><guid isPermaLink="true">https://quantuxblog.com/how-to-make-heart-metrics-work-in-practice</guid><category><![CDATA[metrics]]></category><category><![CDATA[user experience]]></category><category><![CDATA[UX]]></category><category><![CDATA[quantux]]></category><dc:creator><![CDATA[Kerry Rodden]]></dc:creator><pubDate>Tue, 15 Oct 2024 15:42:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/oCSol-lBtVA/upload/80a679f6ef54aceffaf1918f842b2af9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>👋 I'm happy to be making my first contribution to the Quant UX Blog! I'm Chris's co-author on the <a target="_blank" href="https://quantuxbook.com/">Quant UX Research book</a>, and I'm excited to join him in sharing perspectives that complement the book.</p>
<p>I led the early Quant UX Research team at Google, where we came up with the HEART framework to help teams define metrics of user experience. More than a decade ago, I wrote this
<a target="_blank" href="https://library.gv.com/how-to-choose-the-right-ux-metrics-for-your-product-5f46359ab5be">blog post about UX metrics</a>
that introduced HEART to a wider audience outside of Google.</p>
<p>Since then, it has been applied by teams across the tech industry, and has found its way into many introductory resources about user experience and product management, including the popular book <a target="_blank" href="]https://www.oreilly.com/library/view/escaping-the-build/9781491973783/"><em>Escaping the Build Trap</em></a>. Those resources generally cover similar ground to my original blog post, and there is little published material about what happens when teams actually apply HEART to real projects.</p>
<p>In this post, I'll go beyond those basic concepts and share some new perspectives on how to apply HEART in practice, based on material from Chapter 7 of the <a target="_blank" href="https://quantuxbook.com/">Quantitative UX Research book</a>. </p>
<p>It's divided into four sections:</p>
<ul>
<li>What is the HEART framework?</li>
<li>Don't skip the Goals-Signals-Metrics process</li>
<li>Avoid individual pitfalls</li>
<li>Look out for organizational challenges</li>
</ul>
<p>With this knowledge, you'll have a better chance of helping your team reach a successful implementation of user experience metrics.</p>
<hr />
<h2 id="heading-what-is-the-heart-framework">What is the HEART Framework?</h2>
<p>HEART helps teams break down the broad concept of "user experience" into more specific, measurable outcomes. It also encourages teams to consider multiple aspects of the user experience when defining metrics, although it does not cover every possible aspect.</p>
<p>The acronym stands for:</p>
<ol>
<li><p><strong>Happiness</strong>: Measures of user attitudes, often collected via surveys. This might include satisfaction or perceived ease of use.</p>
</li>
<li><p><strong>Engagement</strong>: The level of user involvement with a product, typically measured by frequency, intensity, or depth of interaction. For example, the number of visits during a certain time period, or the usage of key features.</p>
</li>
<li><p><strong>Adoption</strong>: How many new users start using a product or feature. Making an explicit distinction between new and existing users helps a team to understand growth.</p>
</li>
<li><p><strong>Retention</strong>: The rate at which existing users return to the product. This can be thought of as a long-term version of engagement. Some teams focus more specifically on failure to retain, which is known as "churn".</p>
</li>
<li><p><strong>Task success</strong>: The efficiency, effectiveness, and error rate of user actions. This category often yields the most useful metrics for UX changes, provided that task-specific data is available.</p>
</li>
</ol>
<hr />
<h2 id="heading-dont-skip-the-goals-signals-metrics-process">Don't Skip the Goals-Signals-Metrics Process</h2>
<p>HEART 💖 has a fun acronym that makes it easy to remember. Teams often get enthusiastic about it and jump straight to brainstorming metric ideas, because they want to get started on building a dashboard.</p>
<p>However, this is very unlikely to lead to a successful outcome. Metrics are not useful unless they are aligned closely with the team's high-level goals... and many teams are surprisingly unclear about what those are, especially in terms of user experience.</p>
<p>The Goals-Signals-Metrics process is designed to help with this problem by encouraging teams to start by thinking at a higher level.</p>
<ol>
<li><p><strong>Goals</strong>: Using the HEART framework as inspiration, define the overarching objectives for your product or feature. This step involves team discussions to align on priorities and user experience goals, including explicitly addressing the inevitable disagreements. Omit HEART categories that are less relevant to your project.</p>
</li>
<li><p><strong>Signals</strong>: For each goal, identify possible signals — ways that success or failure might manifest in user behavior or attitudes. Map the goals to the data that you are (or could be) collecting about user experience. Consider both the ease of tracking these signals and their likely sensitivity to design changes.</p>
</li>
<li><p><strong>Metrics</strong>: Develop specific, quantifiable measurements based on the signals. This involves steps like figuring out how to analyze the low-level signals (e.g., using averages or percentages) and deciding on appropriate time periods for aggregation.</p>
</li>
</ol>
<p>By following this process, teams can create meaningful metrics that align closely with their product goals and user experience priorities. To give a simple example, a goal of "make the upload process easier" might map to signals relating to completion of each stage of the process, or, alternatively, to responses to an inline survey question about ease of use. The signal about completion could translate to a specific metric like "the percentage of times a user finishes the upload flow successfully, having started it in the past 7 days".</p>
<p>Two important aspects of the process that I want to emphasize:</p>
<ul>
<li><p><strong>You Must Prioritize</strong>: Focus on implementing metrics related to your top goals. It's better to have a few well-chosen metrics than an overwhelming dashboard.</p>
</li>
<li><p><strong>You Must Iterate</strong>: When you've gone through the process the first time, you are not done. As you collect data and gain insights, be prepared to refine your choices over time.</p>
</li>
</ul>
<hr />
<h2 id="heading-avoid-these-individual-pitfalls">Avoid These Individual Pitfalls</h2>
<p>Certain issues come up over and over again for individual quantitative UX researchers when they try to apply HEART and Goals-Signals-Metrics. I still make some of these mistakes myself!</p>
<h3 id="heading-1-neglecting-team-involvement">1. Neglecting Team Involvement</h3>
<p>While it might seem efficient to develop metrics independently, this approach can backfire. Engaging your team throughout the process:</p>
<ul>
<li>Increases ownership and buy-in</li>
<li>Improves the quality of metric ideas</li>
<li>Enhances the impact of the eventual assessments</li>
</ul>
<p>I suggest scheduling a collaborative session to work through the first part of the Goals-Signals-Metrics process with key members of your team: agree on goals, and brainstorm possible signals.</p>
<h3 id="heading-2-starting-too-ambitiously">2. Starting Too Ambitiously</h3>
<p>The HEART framework is most useful when applied to specific projects with engaged teams. Avoid the temptation to create organization-wide dashboards immediately. Instead:</p>
<ul>
<li>Begin with a single, receptive team</li>
<li>Focus on details and learn from the experience</li>
<li>Use this initial project as a case study to inspire other teams</li>
</ul>
<h3 id="heading-3-underestimating-the-rest-of-the-process">3. Underestimating the Rest of the Process</h3>
<p>HEART and Goals-Signals-Metrics are only the beginning of a long process. Just because you used them to come up with a metric idea, that doesn't mean that it's a <em>good</em> or a <em>useful</em> metric, or that it has any correlation with quality of user experience.</p>
<p>Be prepared for:</p>
<ul>
<li>Data analysis to refine your signals and metrics</li>
<li>Iterating on your chosen metrics as you learn more</li>
<li>Implementation challenges, including instrumentation and dashboard creation</li>
</ul>
<p>Allocate sufficient time and resources for these crucial next steps. </p>
<h3 id="heading-4-metric-overload">4. Metric Overload</h3>
<p>While HEART can help you generate numerous ideas, implementing too many metrics can be counterproductive. To avoid overwhelming your team and other stakeholders:</p>
<ul>
<li>Clearly prioritize your most important metrics</li>
<li>Consider implementing secondary metrics on a separate dashboard</li>
<li>Remember that you don't need to use all HEART categories – focus on what's most relevant to your project</li>
</ul>
<hr />
<h2 id="heading-look-out-for-these-organizational-challenges">Look Out For These Organizational Challenges</h2>
<p>Every project takes place in context, and different organizations and teams have different challenges that you can attempt to proactively address, or keep in mind when selecting the right projects to apply HEART.</p>
<h3 id="heading-1-fear-of-evaluation">1. Fear of Evaluation</h3>
<p>Introducing targeted metrics can expose project shortcomings, which may create anxiety within teams. To address this:</p>
<ul>
<li>If you are in a leadership role, foster a culture of learning from failures (e.g., blameless post-mortems); otherwise, prioritize working with teams who already have this kind of culture</li>
<li>Involve stakeholders in the metric definition process to build trust and ownership</li>
<li>Emphasize the value of being informed by data, even when results are unexpected</li>
</ul>
<h3 id="heading-2-single-metric-tunnel-vision">2. Single Metric Tunnel Vision</h3>
<p>While having too many metrics is problematic, focusing on a single metric is also detrimental, but leaders are often drawn to that in an attempt to create clarity and focus. Chris has a separate post about <a target="_blank" href="https://quantuxblog.com/north-star-a-path-to-being-lost">the problems with "North Star" metrics</a>. Remember:</p>
<ul>
<li>No metric can be a perfect representation of user experience</li>
<li>Additional key metrics provide essential checks and balances</li>
<li>Optimizing for one metric can render it useless for evaluation (<a target="_blank" href="https://en.wikipedia.org/wiki/Goodhart%27s_law">Goodhart's Law</a>)</li>
</ul>
<h3 id="heading-3-not-considering-ethical-implications">3. Not Considering Ethical Implications</h3>
<p>UX metrics are only proxies for user experiences. No metric of user engagement can actually identify how truly engaged a user is, or whether that engagement represents a positive experience for them. For example, time spent in a product is often used as a default metric of engagement, but this may not be appropriate for a given product, especially if unhealthy overuse is a possibility.</p>
<p>When implementing metrics:</p>
<ul>
<li>Consider long-term outcomes and potential negative consequences</li>
<li>Use qualitative research to gain deeper insights into actual user experiences</li>
<li>Be willing to iterate on metrics as you learn more about what constitutes a positive user experience</li>
</ul>
<hr />
<h2 id="heading-key-takeaways">Key Takeaways</h2>
<p>HEART is a useful tool to help teams focus on the user experience when defining metrics. Here's a summary of the main things to keep in mind when applying it in practice:</p>
<ul>
<li>Use HEART as a starting point to consider various aspects of user experience, but don't use all of the categories or force goals to fit into them</li>
<li>Follow the Goals-Signals-Metrics process to create meaningful, prioritized metrics</li>
<li>Collaborate with your team to define goals, signals, and metrics</li>
<li>Start small and grow your approach over time</li>
<li>Be prepared for the work that comes after initial metric definition</li>
<li>Prioritize your most important metrics to avoid overload, but don't narrow down to a single metric</li>
<li>Look out for the organizational challenges that come with negative results</li>
<li>Consider ethical implications and long-term outcomes</li>
</ul>
<p>For more detail on all of this, and a case study of a Gmail project, see Chapter 7 of the <a target="_blank" href="https://quantuxbook.com/">Quantitative UX Research book</a>. I'm also available for <a target="_blank" href="https://kerryrodden.com/heart">consulting or speaking</a> on these topics 😎</p>
<p>Have you applied HEART with your team? If you have, I encourage you to write about your experiences, so that the rest of the community can build on what you've learned.</p>
]]></content:encoded></item><item><title><![CDATA[So long 110%]]></title><description><![CDATA[Three weeks ago I "retired" from corporate work. This follows 24 years as a UX researcher at Google (11 years), Microsoft (11 years), and Amazon (2 years), and as a psychologist before that.
This post is mostly an announcement — it's not a retrospect...]]></description><link>https://quantuxblog.com/so-long-110</link><guid isPermaLink="true">https://quantuxblog.com/so-long-110</guid><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Tue, 20 Aug 2024 17:38:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724172762289/8e4dc294-b4ce-443e-97e4-74480df00ca7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Three weeks ago I "retired" from corporate work. This follows 24 years as a UX researcher at Google (11 years), Microsoft (11 years), and Amazon (2 years), and as a psychologist before that.</p>
<p>This post is mostly an announcement — it's not a retrospective, which I'll save for another time. However, I share a couple of updates on what's next.</p>
<hr />
<h2 id="heading-why-now">Why Now?</h2>
<p><strong>Basically, I feel ready</strong>. There is no particular external reason or pressure. I'm 56 years old, and am fortunate to be in good health, with a job that was going well.</p>
<p>On the other hand, after 24 years, plus more years of pre-Tech work, I have accomplished "enough" in the corporate world. The challenges in UX looking forward are no longer as exciting to me as they once were. (That's not a commentary on the UX world — only a comment about my perception.)</p>
<p>BTW, I'll give particular thanks to my most recent colleagues at Amazon Lab 126, where I was very happy. This decision is not a reflection on them or that position. (I can say the same for my 11 years each at Google and Microsoft.)</p>
<p>And I have opportunities apart from corporate work, as I mention next!</p>
<hr />
<h2 id="heading-whats-next">What's Next?</h2>
<p>Many friends have asked me variations of, "What will you do? Don't just quit and watch TV!"</p>
<p>My answer: <strong>thanks and don't worry</strong>! For one thing, I have not turned on a TV set in 5 days (maybe longer, I don't even recall).</p>
<p>More to the point, I will have 4 primary activities keeping me busy:</p>
<ul>
<li><p><strong>Running the Quant UX Association</strong> — I'll lead both the annual conference, <a target="_blank" href="https://quantuxcon.org">Quant UX Con online</a>, and our new <a target="_blank" href="https://quantuxcon.org/classes">in-person training classes</a>.</p>
</li>
<li><p><strong>Other community</strong>: I'm an ordained <a target="_blank" href="https://blueheronzen.org/about/teachers/">Zen priest</a>, and last year I received teaching authorization. With corporate retirement, I can also devote more time to that.<br />  \==&gt; <em><mark>Update April 2025</mark></em>: I am starting the <strong>Tech Community Zen group</strong> for online practice. <a target="_blank" href="https://tczen.org"><strong>More here</strong></a>!</p>
</li>
<li><p><strong>Hobbies</strong>: I have several hobbies. Hobby #1 is boating in the Seattle region and north to British Columbia. We have plans to take our boat all the way to Alaska.</p>
</li>
<li><p><strong>Catching up on writing</strong>. I've overdue on projects such as new editions of my <a target="_blank" href="https://quantuxbook.com">quant book</a> or <a target="_blank" href="https://r-marketing.r-forge.r-project.org">R book</a>, and/or new books and articles. (You'll notice I list writing <em>after</em> the three "fun" activities above.)</p>
</li>
</ul>
<hr />
<h2 id="heading-im-not-going-anywhere-but-keep-in-touch">I'm not going anywhere ... but keep in touch!</h2>
<p>If you haven't already, follow the <a target="_blank" href="https://www.linkedin.com/company/quantuxa">Quant UX Association</a> and <a target="_blank" href="https://www.linkedin.com/in/cnchapman/">me on LinkedIn</a> or <a target="_blank" href="https://bsky.app/profile/cchapman.bsky.social">Bluesky</a>. Also, subscribe to this blog (hit the "follow" button above).</p>
<p>I hope to hear from you ... or even better to see you at <a target="_blank" href="https://quantuxcon.org">Quant UX Con</a> or a Quant class! Cheers,</p>
<p>-- Chris</p>
<p><em>P.S. The cover photo shows me in Shinjuku National Garden in Tokyo, in winter time. Photo credit to my Google colleague, Katie Tzanidou.</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748018480826/b649704b-ea32-47a5-9e2c-afddf0af0040.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Quant UX Interview "Portfolio" Presentations: Recommendation]]></title><description><![CDATA[A reader recently asked me for advice on putting together a Quant UX portfolio. That sparked me to reflect on presentations that are part of a hiring interview process.

I'm talking about research presentations that are often confused with "portfolio...]]></description><link>https://quantuxblog.com/quant-ux-interview-portfolio-presentations-recommendation</link><guid isPermaLink="true">https://quantuxblog.com/quant-ux-interview-portfolio-presentations-recommendation</guid><category><![CDATA[quantux]]></category><category><![CDATA[#Ux research]]></category><category><![CDATA[interview]]></category><dc:creator><![CDATA[Chris Chapman]]></dc:creator><pubDate>Thu, 11 Jul 2024 19:01:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Q80LYxv_Tbs/upload/202a6f32bf8ef2cbb796be7b01af0c90.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A reader recently asked me for advice on putting together a Quant UX portfolio. That sparked me to reflect on presentations that are part of a hiring interview process.</p>
<blockquote>
<p>I'm talking about research presentations that are often confused with "portfolios". They are different from actual "portfolios" that compile many projects and that live online. I doubt whether those are valuable for general tech job applicants (who would look at them?) and in any case, they are different.</p>
<p>However, a presentation as described here could be a key building block in a complete portfolio (if you decide you need one).</p>
</blockquote>
<p>Over the years as a hiring manager and interviewer, I've sat through perhaps 150 interview candidate presentations. I've also observed 1000s of industry research presentations — and of course I've been on the other side as a job candidate. Here are my reflections.</p>
<p>The short version is this: a UX <strong>research presentation should demonstrate how you have defined and answered an interesting research question</strong>. I emphasize "<em>presentation</em>", "<em>defined</em>", "<em>answered</em>", and "<em>interesting</em>". The presentation is about research impact and engagement, <strong>not</strong> about technical proficiency. In this post, I describe the pieces to deliver that.</p>
<p>An event shorter version is this: <strong>are you genuinely enthusiastic about the research you're presenting? If so, that is the best presentation!</strong></p>
<hr />
<h3 id="heading-its-a-presentation-not-a-portfolio">It's a Presentation, not a "Portfolio"</h3>
<p>The term "portfolio" is sometimes misleadingly used to refer to a presentation. It implies that a company wants you to compile an array of materials that describe the range of one's expertise and experience.</p>
<p>That has never been the case anywhere I have worked and hired candidates. Instead, what the hiring team really wants is a clear <strong>presentation</strong> of research. It should be an example of what they might expect from you, if you're hired.</p>
<p>My recommendation is to plan to present 1 case study ... and to have a 2nd case study as a backup. Almost always I find that the 1st case leads to plenty of questions and discussion, but occasionally it doesn't. I have never — not once in 20+ years — observed a need for more than 2 case studies.</p>
<p>I illustrate my points using <strong>slides from my own most recent job presentation</strong> (in early 2022, when I applied for my current job). I <em>don't</em> claim that my slides are awesome or that they are a model ... all I claim is that (a) they illustrate my points, and (b) they helped me land my job (and a quite similar approach helped land my previous job).</p>
<p>Throughout, I encourage you to <strong>focus on UX — user / customer experience — more than any "Quant" aspect</strong>. Impress the audience with what you have to contribute to product research, which is <em>built on, but goes beyond,</em> quant technical expertise.</p>
<hr />
<h3 id="heading-the-audience-has-broad-not-specific-expertise">The Audience has Broad, not Specific Expertise</h3>
<p>Approach a research presentation just like you would in a typical stakeholder meeting with intelligent and interested colleagues who are not researchers — or, if they are researchers, who are not experts in your research area.</p>
<p>At companies like Google, Microsoft, and Amazon, candidates often assume that the interviewers are super-intelligent beings who will know and probe everything. That is not the case!</p>
<p><strong>A better assumption is that interviewers are very smart colleagues who have a different specialization than yours</strong>. They will catch on quickly and ask great questions — and they will immediately sense when you are bluffing — but they also need to be given grounding in your area. If you're an academic, think of them as quick-minded professors from different but related departments.</p>
<p>Explain the basics briefly and prepare to take questions, but don't talk down to the audience. Don't assume that they are experts in your methods or that they want detailed technical explanations (they'll ask if they do). Minimize assumptions and jargon.</p>
<p>Here's how I introduced the topic in my own portfolio with two slides. You'll notice that they are very simple — they highlight key points I made verbally.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720715955873/6127ae37-e102-4dcb-883d-224d65518fbf.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720715872166/384769db-22c2-4aee-ae30-0a3088a1e74d.png" alt class="image--center mx-auto" /></p>
<p>With this kind of framing, the audience knows what I'm going to talk about, and they are immediately focused on a problem statement ... as I discuss in the next section.</p>
<hr />
<h3 id="heading-have-a-narrative-story-with-a-dilemma">Have a Narrative Story with a Dilemma</h3>
<p>This is the most important point: <strong>your case should have a narrative arc</strong> that explains why you did the research — from a business or product point of view — and how it influenced some action.</p>
<p>A good way to frame the arc is with <strong>a dilemma that gets resolved</strong>. For example:</p>
<ul>
<li><p>We needed to change X, but users wanted Y. Here's how research led to a good compromise.</p>
</li>
<li><p>The team was pushing A and yet initial tests suggested that users hated A. Here's how research cleared up the situation.</p>
</li>
<li><p>Our business wanted to expand in B, but the direction was unclear. Here's how research recommended a direction and how it turned out.</p>
</li>
</ul>
<p>Remember that the audience are not specialists in your method. That means that <strong>the dilemma should involve the business and customers, not just research</strong>. For instance, it is <em>not</em> interesting to present, "Technical method A says X but technical method B says Y. I developed technical method C to resolve them." That is uninteresting to anyone except you and a small number of fellow researchers ... unless it connects very clearly to a customer or business problem.</p>
<p>Following the 2 slides above, here's how I laid out a specific dilemma:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720716209876/2ed81ea2-2a82-4cbc-8ac0-faa8f5aa687f.png" alt class="image--center mx-auto" /></p>
<p>Notice that I focus on a tradeoff involving both customers and the business — and again, it is simple and approachable for any intelligent audience.</p>
<p>A research presentation is a <em>job sample</em> and — as shown in my slide above — it should demonstrate the following two crucial skills for UX researchers:</p>
<ul>
<li><p>Focusing on the customer / user</p>
</li>
<li><p>Presenting research clearly to colleagues</p>
</li>
</ul>
<p>To be clear, I do <strong>not</strong> advocate "story telling" in the senses of inventing anything, including artificial drama, sprucing up a presentation with extraneous content or images, or anything like that.</p>
<p>Rather, a presentation should be clear and decision-focused. Clearly focusing ona customer &amp; business dilemma helps to accomplish that.</p>
<hr />
<h3 id="heading-minimize-what-you-say-about-yourself">Minimize What You Say About Yourself</h3>
<p>Too many candidates present multiple slides that detail their academic background, previous work experience, projects they've done, and sometimes things like their hobbies, places of residence, and families.</p>
<p>I suggest having <strong>only 1 screen about yourself</strong> and omit anything that is not particularly relevant or that appears on a resume (such academic background), or is extraneous to the job (such as family and hobbies).</p>
<p>In my most recent portfolio, I did not mention my previous jobs (see resume!) or academic credentials (again, see resume!). Instead, I had <strong>one slide about myself</strong> ... and it framed how I view the landscape of user research, along with some things I've done:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720715681622/e1432908-ceff-4783-b3bd-0d5fbe6c973f.png" alt class="image--center mx-auto" /></p>
<p>Why did I use the 1 slide about "me" in this way? Because:</p>
<ul>
<li><p>This framing lets me share my "philosophy" of end-to-end research</p>
</li>
<li><p>It shows that my focus is not on "me" but on how I approach research</p>
</li>
<li><p>But is also highlights specific experience and competence</p>
</li>
<li><p>It lets me link my research philosophy to the case studies I'll present</p>
</li>
</ul>
<p>I then promised a deep dive into two areas ... which shows how and why the framing of this slide "about me" is more directly relevant to my research as such:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720716731404/ee809361-6713-4ca9-8e58-2eee073af63e.png" alt class="image--center mx-auto" /></p>
<p>It also implicitly communicates, "I'm showing 2 things but I do much more."</p>
<hr />
<h3 id="heading-dont-include-confidential-information">Don't Include Confidential Information</h3>
<p>A common — and in my opinion, serious — mistake is to include information that is confidential to your current or previous employer.</p>
<p>I've repeatedly seen candidates who present information including:</p>
<ul>
<li><p>Screen mockups and images of unreleased products</p>
</li>
<li><p>Photos of customers and/or team members</p>
</li>
<li><p>Names of customers and/or team members</p>
</li>
<li><p>Statistical analyses of user needs, product behaviors, purchases, willingness to pay, etc., that come from internal, confidential data</p>
</li>
<li><p>Internal data about finances, sales, staffing, or other business metrics</p>
</li>
</ul>
<p>There are two problems with presenting such confidential information. First, it violates your employment agreement ... and the interviewing company will wonder whether you will do the same thing with them! <strong>Second, it demonstrates a lack of attention or competence with a core professional skill for UX researchers: ethical research</strong>.</p>
<p><em>But wait?!</em> What can you show if you can't show actual results? <strong>Your audience will understand and appreciate the need for confidentiality.</strong> It is OK to have blurred images, hypothetical screen mock ups, similar but disguised products, stock photos of users, and altered statistics that communicate a parallel result.</p>
<p>Even better, <strong>if you get approval to publish research periodically in your career, you'll have a collection that is perfectly OK to share</strong>. That's not required but is nice when it occurs. Following is an example from my presentation (a similar example is shown in the Case 1 intro above). This slide also shows how to credit colleagues.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720717708330/48599548-a294-41e1-83e0-4caff7053f23.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-resolve-the-narrative-arc-with-a-decision-and-impact">Resolve the Narrative Arc with a Decision and Impact</h3>
<p><strong>Every good story needs a clear resolution</strong>. Your case study should start with a clear dilemma or decision to be made, as noted above. Then you should end with a resolution of that dilemma.</p>
<p>I'm not going to recap my entire presentation, but I will skip ahead to the key research result for Case #1. I used <a target="_blank" href="https://www.researchgate.net/publication/261697842_Game_theory_and_conjoint_analysis_using_choice_data_for_strategic_decisions">game theory + conjoint analysis</a> to answer the question, and presented the final result like this (after walking through the pieces):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720717941966/04086d8c-c2e1-4251-8910-08a55db319c6.png" alt class="image--center mx-auto" /></p>
<p>You'll notice that this slide is <strong>technical but also quite approachable</strong> (especially after building up some of the components, which I did in previous slides). At the same time it is not particularly complex. The goal is to be clear and convincing, <strong>including only what is needed to get the point across</strong>. That has the nice side effect of making the audience feel smarter (which will make them like you!)</p>
<p><strong>That slide directly answers the dilemma</strong> that I started with — "yes, we should invest in developing the new feature" — and it set up the next slide about research impact:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720718121205/5275a457-e85c-4192-a9bd-68813f4467fb.png" alt class="image--center mx-auto" /></p>
<p>Notice again that I focused clearly on the business and the customers. And I also worked in a not-so-subtle plug for listening to users through research!</p>
<hr />
<h3 id="heading-for-more">For More</h3>
<p>Do you want to see my complete job presentation? I presented it live at <a target="_blank" href="https://quantuxcon.org">Quant UX Con</a> 2022 (after my interview) and the <a target="_blank" href="https://drive.google.com/file/d/1NehSek1WjKx0Jhf5qipFNLyb6-GXQM9r/view?usp=sharing"><strong>complete PDF is here</strong></a>.</p>
<p>Does something here worry you, because you didn't "get it right" in a recent presentation? <strong>No one ever gets these things completely right</strong> for one simple reason: <em>a portfolio review is an interactive presentation</em> ... and the other side is 50% of the equation. But that side is unknowable in advance.</p>
<p>Sometimes the interviewers are awesome (<em>cue</em>: the colleagues who interviewed me at Microsoft, Google, and Amazon). But sometimes interviewers are jerks (stories I've heard) — and really, it's better to find that out before taking a job. Don't worry about it! Just update your expectations and improve your presentation for the next round.</p>
<p>I hope something here will be useful for you in putting together a Quant UX research presentation. Thanks for reading!</p>
<p><a target="_blank" href="https://notbyai.fyi"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746999945541/9d224843-9e9f-44cc-98c5-276915794420.png" alt class="image--center mx-auto" /></a></p>
]]></content:encoded></item></channel></rss>