<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="https://ajcajc.com/xml/base.min.xml"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Tils on ajcajc.com</title><link>https://ajcajc.com/til/</link><description>Recent content in Tils on ajcajc.com</description><generator>Hugo 0.137.1</generator><language>en-us</language><lastBuildDate>Wed, 04 Nov 2026 08:51:55 -0800</lastBuildDate><atom:link rel="self" href="https://ajcajc.com/til/feed.xml" type="application/rss+xml"/><item><title>2025 Year in Review</title><link>https://ajcajc.com/year-review-2025/</link><pubDate>Wed, 31 Dec 2025 00:00:00 +0000</pubDate><guid>https://ajcajc.com/year-review-2025/</guid><description>&lt;p>I had a wonderful year.
I got to travel, read a bunch, cook a bunch, play new video games, attend theater shows, watch movies, spend time with friends and loved ones, and best of all get a puppy.
I&amp;rsquo;ll try and keep this write-up at a surface level, since I&amp;rsquo;d rather finish it instead of only getting halfway.&lt;/p>
&lt;h1 id="travel">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/year-review-2025/#travel">Travel&lt;/a>
&lt;/h1>
&lt;h2 id="peru">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/year-review-2025/#peru">Peru&lt;/a>
&lt;/h2>
&lt;p>I travelled to Peru in June with some friends.
We did the one-day Inca trail which was a lot of fun.
I enjoyed the hike up from the river to the classic viewpoint of the entire city.
The &amp;ldquo;monkey steps&amp;rdquo; right before the end are a lot of fun; they&amp;rsquo;re just really tall steps.&lt;/p>
&lt;p>Prior to that, we did a one-night trek through Lares, a less-trafficked part of the country.
It was stunning.
There were alpacas and other farm animals all along the trail, beautiful mountain scenery, and a fun, knowledgeable guide to tell us all about the area.
The trekking company, Sam Travel Peru, pulled out all the stops.
They cooked us delicious, healthy meals, and we slept in the most posh tent I&amp;rsquo;ve ever experienced.&lt;/p>
&lt;p>For the other half of the trip, we stayed at a jungle lodge near the Amazon river.
It was a nice balance, because we mainly relaxed on hammocks and did boat excursions out on the water.
The jungle is truly alive, in a bigger sense than &amp;ldquo;there&amp;rsquo;s a ton of wildlife throughout it&amp;rdquo;.
The sound of birds among the trees was incredible, there were so many unique sounds.
I also had the best mango of my life there :)&lt;/p>
&lt;h2 id="washington-dc">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/year-review-2025/#washington-dc">Washington D.C.&lt;/a>
&lt;/h2>
&lt;p>I visited DC for a weekend.
The museums were world-class.
I particularly enjoyed the National Portrait Gallery (and the other museum that&amp;rsquo;s in the same building).
There was an exhibit on all the presidents and short blurbs about what they were known for.
Trump&amp;rsquo;s blurb talked about January 6 for a bit, and later this year he forced them to take it down.
I&amp;rsquo;d like to think we&amp;rsquo;ll collectively refuse to forget it, but since (at least in corporate media) we&amp;rsquo;ve memory-holed GW Bush&amp;rsquo;s crimes, I&amp;rsquo;m not holding my breath.
Regardless of all that, it was an impressive tapestry of those who did their part in shaping American history and culture.&lt;/p>
&lt;p>The Natural History Museum was amazing.
It really scratched that science-y part of my brain that always enjoyed reading National Geographic and marveling at the rich biodiversity of our planet.&lt;/p>
&lt;p>Both of these museums were free to enter!
That shows that there have always been those who want to enrich our collective understanding of the world.
I&amp;rsquo;m grateful for that.&lt;/p>
&lt;h1 id="media">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/year-review-2025/#media">Media&lt;/a>
&lt;/h1>
&lt;h2 id="reading">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/year-review-2025/#reading">Reading&lt;/a>
&lt;/h2>
&lt;p>I read the following books:&lt;/p>
&lt;ul>
&lt;li>&lt;em>Bullshit Jobs: A Theory&lt;/em> by David Graeber
&lt;ul>
&lt;li>Please read this if you ever stop to think about how certain jobs simply should not exist&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;em>Palo Alto: A History of California, Capitalism, and the World&lt;/em> by Malcolm Harris
&lt;ul>
&lt;li>Kind of felt like pop history, but I got a lot out of it. I appreciate that the author avoids a dispassionate voice and picks a side&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;em>American Fascists: The Christian Right and the War on America&lt;/em> by Chris Hedges
&lt;ul>
&lt;li>Chris Hedges blog posts today can get extremely doomy-gloomy, but I often think he&amp;rsquo;s right. His book from almost twenty years ago does a great job predicting the rise of the Christian right. Look who&amp;rsquo;s in charge of the Pentagon today&amp;hellip;&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;em>The Golden Gate&lt;/em> by Amy Chua
&lt;ul>
&lt;li>Awesome whodunit that presents the Bay Area from minority viewpoints in the 1940&amp;rsquo;s, near the end of WW2&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;em>Rich Girl Nation: Taking Charge of Our Financial Futures&lt;/em> by Katie Gatti Tassin
&lt;ul>
&lt;li>The author is a total personal-finance nerd, and she passionately places that in a larger economic/societal lens&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;em>Mort&lt;/em> by Terry Pratchett and &lt;em>Sourcery&lt;/em> by Terry Pratchett
&lt;ul>
&lt;li>I love these Discworld novels. I preferred Mort over Sourcery, since I felt like I was a bit lost in some parts of Sourcery. The characters are all great though&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;em>Automatic Noodle&lt;/em> by Annalee Newitz
&lt;ul>
&lt;li>Cozy sci-fi! Set in a future, post-war San Francisco, it follows a group of robots who open a noodle restaurant. I wish it was longer!&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;em>Enshittification: Why Everything Suddenly Got Worse and What to Do About It&lt;/em> by Cory Doctorow
&lt;ul>
&lt;li>I am a Cory Doctorow stan. While he can get repetitive in his writing between posts, this book is a great sum-up/intro of what he&amp;rsquo;s been talking about for years&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;em>Death&amp;rsquo;s End&lt;/em> by Cixin Liu
&lt;ul>
&lt;li>This is the end of the Three Body Problem Trilogy. It was overall a really fun, imaginative take on humanity working over the course of hundreds of years to avert extinction. It got totally out there as centuries passed, and I was here for it.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;em>A Court of Thorns and Roses&lt;/em> by Sarah J. Maas
&lt;ul>
&lt;li>Great story and I liked the setting! I wanted to see what all the hype was about. I did not finish the sequel, unfortunately&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;em>Dawn&lt;/em> by Octavia E. Butler
&lt;ul>
&lt;li>Sex with aliens, but not in the way you&amp;rsquo;re thinking. I totally respect Butler for taking a big swing with this, and I liked parts of it. However, I didn&amp;rsquo;t like it enough to read the rest of the trilogy&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;em>Chasing Shadows: Cyber Espionage, Subversion, and the Global Fight for Democracy&lt;/em> by Ronald J. Deibert
&lt;ul>
&lt;li>Saw Ron talk at a book signing. He&amp;rsquo;s been instrumental in the citizen-led against cyber-mercenaries. Crazy stories about his time at Citizen Lab&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;em>Do Androids Dream of Electric Sheep?&lt;/em> by Philip K. Dick
&lt;ul>
&lt;li>A sci-fi classic. I&amp;rsquo;m glad I finally read it! I can totally see how it&amp;rsquo;s influenced other sci-fi stories&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;em>Careless People: A Cautionary Tale of Power, Greed, and Lost Idealism&lt;/em> by Sarah Wynn-Williams
&lt;ul>
&lt;li>Holy crap, read this if you&amp;rsquo;ve ever thought, &lt;em>Man, that Mark Zuckerberg sucks&lt;/em>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;em>Annihilation&lt;/em> by Jeff VanderMeer
&lt;ul>
&lt;li>A mind-bending novel where the main character joins an expedition into a mysterious abandoned zone. Loved it, though I won&amp;rsquo;t read the sequels after hearing they&amp;rsquo;re not as good.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="movies-games-and-theater">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/year-review-2025/#movies-games-and-theater">Movies, Games, and Theater&lt;/a>
&lt;/h2>
&lt;p>Favorite movie: Sinners. Such a fun, unique movie. I wish it wasn&amp;rsquo;t billed as &amp;ldquo;horror&amp;rdquo;, otherwise I would&amp;rsquo;ve seen it in theaters where it would&amp;rsquo;ve been twice as good.&lt;/p>
&lt;p>Favorite video game: Hollow Knight on the Steam Deck. Getting ready to join the hype train late for Silksong.&lt;/p>
&lt;p>Favorite musical: Les Misérables. Such powerful ensemble songs. It made me cry and hope for a better world.&lt;/p>
&lt;h2 id="board-games">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/year-review-2025/#board-games">Board Games&lt;/a>
&lt;/h2>
&lt;p>I had a lot of fun playing board games with friends this year.
&amp;ldquo;Azul&amp;rdquo; is a go-to game, but we also really liked &amp;ldquo;Wingspan&amp;rdquo;.
For a fun, quick group card game, try &amp;ldquo;Trio&amp;rdquo;, also called &amp;ldquo;Nana&amp;rdquo;.&lt;/p>
&lt;p>Honorable mention for a new game I was gifted for Christmas, &amp;ldquo;Ra&amp;rdquo;.
It&amp;rsquo;s a fun auction + game theory style game, though I think it&amp;rsquo;s a bit too long.
Maybe we all just play it too slowly.&lt;/p>
&lt;h1 id="puppy">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/year-review-2025/#puppy">Puppy&lt;/a>
&lt;/h1>
&lt;p>The puppy we got this summer has filled lot of my free time.
I was actually apprehensive about that happening, but like all dads who say &amp;ldquo;No I don&amp;rsquo;t want a dog&amp;rdquo;, I have fallen in love with this dog.&lt;/p>
&lt;p>I won&amp;rsquo;t post a pic because I&amp;rsquo;m a relatively private person (also because he&amp;rsquo;s way too cute), but he&amp;rsquo;s been a lot of fun for me and my partner.
I even had fun making him a website where I post a photo every day.
6 months in, I&amp;rsquo;m still posting a photo/video every day, and friends/family still check in on it :)&lt;/p>
&lt;h1 id="conclusion">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/year-review-2025/#conclusion">Conclusion&lt;/a>
&lt;/h1>
&lt;p>Overall, I&amp;rsquo;m very grateful for my health, friends, family, safety and comfort.
I&amp;rsquo;d like to do more work this next year with writing down my thoughts, since that&amp;rsquo;ll help me express them in person more.
That probably won&amp;rsquo;t be online writing, but we&amp;rsquo;ll see!&lt;/p></description></item><item><title>Hannukah of Data</title><link>https://ajcajc.com/hannukah-of-data/</link><pubDate>Sun, 24 Nov 2024 15:27:08 -0800</pubDate><guid>https://ajcajc.com/hannukah-of-data/</guid><description>&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;!-- raw HTML omitted -->
&lt;p>&lt;em>&lt;a href="https://hanukkah.bluebird.sh/5784/">Hannukah of Data: Noah&amp;rsquo;s Rug&lt;/a>&lt;/em> is
a super fun data puzzle where you must sift through a database from
Noah&amp;rsquo;s Market, &amp;ldquo;a bustling mom-and-pop everything store in Manhattan&amp;rdquo;,
and find past customers who can help you find Noah&amp;rsquo;s Rug.&lt;/p>
&lt;p>I decided to solve these puzzles with Python and
&lt;a href="https://docs.pola.rs/">Polars&lt;/a>, a new DataFrame library which seems
more intuitive to me than Pandas.&lt;/p>
&lt;p>I consider myself beginner-intermediate with SQL and general data
querying, so some of my solutions are long-winded where others&amp;rsquo; would be
more concise. I still solved them all though!&lt;/p>
&lt;p>Part 0 is getting access to the database files, which I&amp;rsquo;ll leave as an
exercise to the reader.&lt;/p>
&lt;p>I&amp;rsquo;ll also be leaving most of the challenge description/story out of
this, but I really recommend playing along because it&amp;rsquo;s funny story that
plays out.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Load data&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> polars &lt;span style="color:#66d9ef">as&lt;/span> pl
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>customers &lt;span style="color:#f92672">=&lt;/span> pl&lt;span style="color:#f92672">.&lt;/span>read_csv(&lt;span style="color:#e6db74">&amp;#34;data/noahs-customers.csv&amp;#34;&lt;/span>, try_parse_dates&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>orders_items &lt;span style="color:#f92672">=&lt;/span> pl&lt;span style="color:#f92672">.&lt;/span>read_csv(&lt;span style="color:#e6db74">&amp;#34;data/noahs-orders_items.csv&amp;#34;&lt;/span>, try_parse_dates&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>orders &lt;span style="color:#f92672">=&lt;/span> pl&lt;span style="color:#f92672">.&lt;/span>read_csv(&lt;span style="color:#e6db74">&amp;#34;data/noahs-orders.csv&amp;#34;&lt;/span>, try_parse_dates&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>products &lt;span style="color:#f92672">=&lt;/span> pl&lt;span style="color:#f92672">.&lt;/span>read_csv(&lt;span style="color:#e6db74">&amp;#34;data/noahs-products.csv&amp;#34;&lt;/span>, try_parse_dates&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h1 id="part-1-the-investigator">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/hannukah-of-data/#part-1-the-investigator">Part 1: The Investigator&lt;/a>
&lt;/h1>
&lt;p>The goal is to find the customer whose phone number is equivalent to
their last name spelled out on a phone keypad.&lt;/p>
&lt;p>First, I&amp;rsquo;ll need a function that converts a string (last name) to a
phone number. For example, &amp;ldquo;SAM&amp;rdquo; would be &amp;ldquo;726&amp;rdquo; and &amp;ldquo;NOAH&amp;rdquo; would be
&amp;ldquo;6624&amp;rdquo;.&lt;/p>
&lt;p>Then, I&amp;rsquo;ll need to run that function on everyone&amp;rsquo;s last name and see if
it matches their phone number.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">letter_to_number&lt;/span>(letter: str) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> str:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> len(letter) &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">raise&lt;/span> &lt;span style="color:#a6e22e">ValueError&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;You can only pass one letter at a time&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> letter &lt;span style="color:#f92672">=&lt;/span> letter&lt;span style="color:#f92672">.&lt;/span>lower()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> letter &lt;span style="color:#f92672">in&lt;/span> &lt;span style="color:#e6db74">&amp;#34;abc&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;2&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> letter &lt;span style="color:#f92672">in&lt;/span> &lt;span style="color:#e6db74">&amp;#34;def&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;3&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> letter &lt;span style="color:#f92672">in&lt;/span> &lt;span style="color:#e6db74">&amp;#34;ghi&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;4&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> letter &lt;span style="color:#f92672">in&lt;/span> &lt;span style="color:#e6db74">&amp;#34;jkl&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;5&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> letter &lt;span style="color:#f92672">in&lt;/span> &lt;span style="color:#e6db74">&amp;#34;mno&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;6&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> letter &lt;span style="color:#f92672">in&lt;/span> &lt;span style="color:#e6db74">&amp;#34;pqrs&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;7&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> letter &lt;span style="color:#f92672">in&lt;/span> &lt;span style="color:#e6db74">&amp;#34;tuv&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;8&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">elif&lt;/span> letter &lt;span style="color:#f92672">in&lt;/span> &lt;span style="color:#e6db74">&amp;#34;wxyz&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;9&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">name_to_number&lt;/span>(name: str) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> str:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>&lt;span style="color:#f92672">.&lt;/span>join([letter_to_number(l) &lt;span style="color:#66d9ef">for&lt;/span> l &lt;span style="color:#f92672">in&lt;/span> name &lt;span style="color:#66d9ef">if&lt;/span> l&lt;span style="color:#f92672">.&lt;/span>isalpha()])&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Solution&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>last_names_translated &lt;span style="color:#f92672">=&lt;/span> customers&lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>str&lt;span style="color:#f92672">.&lt;/span>extract(&lt;span style="color:#e6db74">r&lt;/span>&lt;span style="color:#e6db74">&amp;#34;(.+) (.+)&amp;#34;&lt;/span>, group_index&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>map_elements(name_to_number, return_dtype&lt;span style="color:#f92672">=&lt;/span>pl&lt;span style="color:#f92672">.&lt;/span>String)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;name_number&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;phone&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> last_names_translated&lt;span style="color:#f92672">.&lt;/span>filter(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;name_number&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;phone&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>str&lt;span style="color:#f92672">.&lt;/span>replace_all(&lt;span style="color:#e6db74">&amp;#34;-&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (1, 3)
┌─────────────┬────────────────┬──────────────┐
│ name_number ┆ name ┆ phone │
│ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str │
╞═════════════╪════════════════╪══════════════╡
│ 8266362286 ┆ Sam Tannenbaum ┆ 826-636-2286 │
└─────────────┴────────────────┴──────────────┘
&lt;/code>&lt;/pre>
&lt;p>The above code selects the &amp;ldquo;name&amp;rdquo; column in the customers table, takes
the last name, and runs the &lt;code>name_to_number&lt;/code> function function on it.
The second line then filters all the rows where the above &amp;ldquo;last name
translated to phone number&amp;rdquo; (here I called the new column &lt;code>name_number&lt;/code>)
is equal to the the phone number. Luckily there was only one person!&lt;/p>
&lt;h1 id="part-2-the-contractor">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/hannukah-of-data/#part-2-the-contractor">Part 2: The Contractor&lt;/a>
&lt;/h1>
&lt;p>Finding the phone number in Part 1 unlocks Part 2, where you need to
find a contractor&amp;rsquo;s phone number.&lt;/p>
&lt;blockquote>
&lt;p>&amp;hellip; &lt;strong>they usually talked about the project over coffee and bagels at
Noah&amp;rsquo;s before handing off the item to be cleaned. The contractors
would pick up the tab and expense it, along with their cleaning
supplies.&lt;/strong>&lt;/p>
&lt;p>&amp;ldquo;So this rug was apparently one of those special projects. The claim
ticket said &amp;lsquo;2017 JP&amp;rsquo;. &lt;strong>&amp;lsquo;2017&amp;rsquo; is the year the item was brought in,
and &amp;lsquo;JP&amp;rsquo; is the initials of the contractor.&lt;/strong>&lt;/p>
&lt;/blockquote>
&lt;p>First I&amp;rsquo;ll find all the customers whose initials are JP. How many could
there be?&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>jp_customers &lt;span style="color:#f92672">=&lt;/span> customers&lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>str&lt;span style="color:#f92672">.&lt;/span>contains(&lt;span style="color:#e6db74">&amp;#34;^J.+ P.+$&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(jp_customers&lt;span style="color:#f92672">.&lt;/span>height)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>72
&lt;/code>&lt;/pre>
&lt;p>How many orders did they collectively place in 2017?&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Get all of the orders (including items) from JP in 2017&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>orders_2017 &lt;span style="color:#f92672">=&lt;/span> orders&lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;ordered&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>dt&lt;span style="color:#f92672">.&lt;/span>year() &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">2017&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>join(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> jp_customers, on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>orders_items_2017 &lt;span style="color:#f92672">=&lt;/span> orders_items&lt;span style="color:#f92672">.&lt;/span>join(orders_2017, on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;orderid&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(len(orders_items_2017))&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>486
&lt;/code>&lt;/pre>
&lt;p>Ok, so I need to find the customer who buys coffee, bagels, and cleaning
supplies. Visually inspecting the products table shows that I only need
to filter on products with descriptions containing &amp;ldquo;bagel&amp;rdquo;, &amp;ldquo;coffee&amp;rdquo;,
and &amp;ldquo;cleaner&amp;rdquo;.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>coffee_bagels_cleaners &lt;span style="color:#f92672">=&lt;/span> products&lt;span style="color:#f92672">.&lt;/span>filter(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;desc&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>str&lt;span style="color:#f92672">.&lt;/span>contains(&lt;span style="color:#e6db74">&amp;#34;Bagel|Coffee|Cleaner&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(coffee_bagels_cleaners)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (4, 4)
┌─────────┬───────────────┬────────────────┬───────────────┐
│ sku ┆ desc ┆ wholesale_cost ┆ dims_cm │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ f64 ┆ str │
╞═════════╪═══════════════╪════════════════╪═══════════════╡
│ BKY1573 ┆ Sesame Bagel ┆ 1.02 ┆ 11.9|4.7|0.9 │
│ HOM2761 ┆ Rug Cleaner ┆ 1.43 ┆ 19.6|11.7|0.2 │
│ BKY5717 ┆ Caraway Bagel ┆ 1.03 ┆ 11.3|2.3|1.6 │
│ DLI8820 ┆ Coffee, Drip ┆ 1.44 ┆ 9.6|7.8|0.7 │
└─────────┴───────────────┴────────────────┴───────────────┘
&lt;/code>&lt;/pre>
&lt;p>I want all the order ids where coffee &lt;strong>AND&lt;/strong> bagels &lt;strong>AND&lt;/strong> cleaner
were purchased. I&amp;rsquo;ll take the orders, collect all the SKUs in the order
into a list, and only keep the rows with lists that have three unique
SKUs.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>potential_contractor_orders &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> orders_items_2017&lt;span style="color:#f92672">.&lt;/span>join(coffee_bagels_cleaners, on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;sku&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>group_by(&lt;span style="color:#e6db74">&amp;#34;orderid&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>agg(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;sku&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>potential_contractor_orders_three_or_more &lt;span style="color:#f92672">=&lt;/span> potential_contractor_orders&lt;span style="color:#f92672">.&lt;/span>filter(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;sku&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>list&lt;span style="color:#f92672">.&lt;/span>n_unique() &lt;span style="color:#f92672">&amp;gt;=&lt;/span> &lt;span style="color:#ae81ff">3&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(potential_contractor_orders_three_or_more)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (1, 2)
┌─────────┬─────────────────────────────────┐
│ orderid ┆ sku │
│ --- ┆ --- │
│ i64 ┆ list[str] │
╞═════════╪═════════════════════════════════╡
│ 7459 ┆ [&amp;quot;DLI8820&amp;quot;, &amp;quot;HOM2761&amp;quot;, &amp;quot;BKY157… │
└─────────┴─────────────────────────────────┘
&lt;/code>&lt;/pre>
&lt;p>Apparently there&amp;rsquo;s only one person with initals JP who bought those
items in one order in 2017.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>print(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> potential_contractor_orders_three_or_more&lt;span style="color:#f92672">.&lt;/span>join(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> orders_2017, on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;orderid&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>join(customers, on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;phone&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (1, 3)
┌────────────┬─────────────────┬──────────────┐
│ customerid ┆ name ┆ phone │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ str │
╞════════════╪═════════════════╪══════════════╡
│ 1475 ┆ Joshua Peterson ┆ 332-274-4185 │
└────────────┴─────────────────┴──────────────┘
&lt;/code>&lt;/pre>
&lt;h1 id="part-3-the-neighbor">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/hannukah-of-data/#part-3-the-neighbor">Part 3: The Neighbor&lt;/a>
&lt;/h1>
&lt;p>The contractor passed the rug along to a neighbor. Here&amp;rsquo;s the important
info for finding them:&lt;/p>
&lt;blockquote>
&lt;p>I gave it to this guy who lived in my neighborhood&amp;hellip; he was a Cancer
born in the year of the Rabbit&amp;hellip;&lt;/p>
&lt;/blockquote>
&lt;p>So I&amp;rsquo;ll need to find the customer who: - lives in Joshua Peterson&amp;rsquo;s
neighborhood - is a Cancer: born between the dates of June 21st and July
22nd (&lt;a href="https://www.horoscope.com/zodiac-signs/cancer">source&lt;/a>) - was
born in the year of the Rabbit: any of the following years: 2023, 2011,
1999, 1987, 1975, 1963, and 1951
(&lt;a href="https://chinesenewyear.net/zodiac/rabbit/">source&lt;/a>)&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> datetime &lt;span style="color:#66d9ef">as&lt;/span> dt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Phone number is from part 2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>joshua_peterson_citystatezip &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customers&lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;phone&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;332-274-4185&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;citystatezip&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>item()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>neighbors &lt;span style="color:#f92672">=&lt;/span> customers&lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;citystatezip&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> joshua_peterson_citystatezip)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>june_21_through_31 &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;birthdate&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>dt&lt;span style="color:#f92672">.&lt;/span>month() &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">6&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;birthdate&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>dt&lt;span style="color:#f92672">.&lt;/span>day()&lt;span style="color:#f92672">.&lt;/span>is_in(list(range(&lt;span style="color:#ae81ff">21&lt;/span>, &lt;span style="color:#ae81ff">32&lt;/span>))),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>july_1_through_22 &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;birthdate&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>dt&lt;span style="color:#f92672">.&lt;/span>month() &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">7&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;birthdate&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>dt&lt;span style="color:#f92672">.&lt;/span>day()&lt;span style="color:#f92672">.&lt;/span>is_in(list(range(&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">23&lt;/span>))),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># I&amp;#39;d love to learn out how to OR these two filters together rather than concat&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>neighbors_june_cancer &lt;span style="color:#f92672">=&lt;/span> neighbors&lt;span style="color:#f92672">.&lt;/span>filter(june_21_through_31)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>neighbors_july_cancer &lt;span style="color:#f92672">=&lt;/span> neighbors&lt;span style="color:#f92672">.&lt;/span>filter(july_1_through_22)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>neighbors_cancer &lt;span style="color:#f92672">=&lt;/span> pl&lt;span style="color:#f92672">.&lt;/span>concat(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [neighbors_june_cancer, neighbors_july_cancer], how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;vertical&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(neighbors_cancer&lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;phone&amp;#34;&lt;/span>))&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (2, 3)
┌────────────┬───────────────┬──────────────┐
│ customerid ┆ name ┆ phone │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ str │
╞════════════╪═══════════════╪══════════════╡
│ 2550 ┆ Robert Morton ┆ 917-288-9635 │
│ 9120 ┆ Max Moore ┆ 315-291-6374 │
└────────────┴───────────────┴──────────────┘
&lt;/code>&lt;/pre>
&lt;p>Wow, that narrows it down to two neighbors who are Cancers! At this
point I could manually inspect their birthdate, but I can just as easily
query it.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>years_of_the_rabbit &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#ae81ff">2023&lt;/span>, &lt;span style="color:#ae81ff">2011&lt;/span>, &lt;span style="color:#ae81ff">1999&lt;/span>, &lt;span style="color:#ae81ff">1987&lt;/span>, &lt;span style="color:#ae81ff">1975&lt;/span>, &lt;span style="color:#ae81ff">1963&lt;/span>, &lt;span style="color:#ae81ff">1951&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> neighbors_cancer&lt;span style="color:#f92672">.&lt;/span>filter(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;birthdate&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>dt&lt;span style="color:#f92672">.&lt;/span>year()&lt;span style="color:#f92672">.&lt;/span>is_in(years_of_the_rabbit)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )&lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;phone&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (1, 3)
┌────────────┬───────────────┬──────────────┐
│ customerid ┆ name ┆ phone │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ str │
╞════════════╪═══════════════╪══════════════╡
│ 2550 ┆ Robert Morton ┆ 917-288-9635 │
└────────────┴───────────────┴──────────────┘
&lt;/code>&lt;/pre>
&lt;h1 id="part-4-the-early-bird">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/hannukah-of-data/#part-4-the-early-bird">Part 4: The Early Bird&lt;/a>
&lt;/h1>
&lt;p>The neighbor passed the rug along to a woman he met on Tinder.&lt;/p>
&lt;blockquote>
&lt;p>Thankfully, this woman I met on Tinder came over at &lt;strong>5am&lt;/strong> with her
bike chain repair kit and some pastries from Noah&amp;rsquo;s. Apparently she
liked to get up before dawn and &lt;strong>claim the first pastries that came
out of the oven&lt;/strong>.&lt;/p>
&lt;/blockquote>
&lt;p>So I need to find the customer who most often is the first to order
pastries before 5am.&lt;/p>
&lt;p>It seems that the sku in the &amp;ldquo;products&amp;rdquo; table corresponds the department
of the store it&amp;rsquo;s from. In this case, bakery items start with &amp;lsquo;BKY&amp;rsquo;.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>bakery_items &lt;span style="color:#f92672">=&lt;/span> products&lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;sku&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>str&lt;span style="color:#f92672">.&lt;/span>starts_with(&lt;span style="color:#e6db74">&amp;#34;BKY&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(bakery_items)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (20, 4)
┌─────────┬─────────────────────────┬────────────────┬────────────────┐
│ sku ┆ desc ┆ wholesale_cost ┆ dims_cm │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ f64 ┆ str │
╞═════════╪═════════════════════════╪════════════════╪════════════════╡
│ BKY1573 ┆ Sesame Bagel ┆ 1.02 ┆ 11.9|4.7|0.9 │
│ BKY1679 ┆ Sesame Twist ┆ 1.02 ┆ 6.9|6.2|1.6 │
│ BKY2114 ┆ Poppyseed Mandelbrot ┆ 1.05 ┆ 13.4|0.7|0.4 │
│ BKY2596 ┆ Poppyseed Linzer Cookie ┆ 1.04 ┆ 16.0|7.5|5.0 │
│ BKY4004 ┆ Raspberry Sufganiah ┆ 1.02 ┆ 18.7|10.6|3.3 │
│ … ┆ … ┆ … ┆ … │
│ BKY7535 ┆ Poppyseed Sufganiah ┆ 0.85 ┆ 16.7|11.1|3.4 │
│ BKY7716 ┆ Caraway Twist ┆ 1.12 ┆ 12.9|9.4|6.7 │
│ BKY8544 ┆ Raspberry Babka ┆ 0.88 ┆ 15.4|14.1|14.0 │
│ BKY8935 ┆ Caraway Bialy ┆ 0.94 ┆ 15.4|14.5|1.2 │
│ BKY9551 ┆ Sesame Puff ┆ 1.03 ┆ 5.0|4.2|2.3 │
└─────────┴─────────────────────────┴────────────────┴────────────────┘
&lt;/code>&lt;/pre>
&lt;p>Now I&amp;rsquo;ll find the customer who places bakery orders before 5am the most
often.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>bakery_items_orders &lt;span style="color:#f92672">=&lt;/span> orders_items&lt;span style="color:#f92672">.&lt;/span>join(bakery_items, on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;sku&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>bakery_orders &lt;span style="color:#f92672">=&lt;/span> orders&lt;span style="color:#f92672">.&lt;/span>join(bakery_items_orders, on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;orderid&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>early_bird_customer_id &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> bakery_orders&lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;ordered&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>dt&lt;span style="color:#f92672">.&lt;/span>hour() &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">5&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>group_by(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>agg(pl&lt;span style="color:#f92672">.&lt;/span>len())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>top_k(&lt;span style="color:#ae81ff">1&lt;/span>, by&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;len&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>item(&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customers&lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> early_bird_customer_id)&lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;phone&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (1, 3)
┌────────────┬──────────────┬──────────────┐
│ customerid ┆ name ┆ phone │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ str │
╞════════════╪══════════════╪══════════════╡
│ 2749 ┆ Renee Harmon ┆ 607-231-3605 │
└────────────┴──────────────┴──────────────┘
&lt;/code>&lt;/pre>
&lt;h1 id="part-5-the-cat-lady">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/hannukah-of-data/#part-5-the-cat-lady">Part 5: The Cat Lady&lt;/a>
&lt;/h1>
&lt;blockquote>
&lt;p>a woman in Staten Island came to pick it up&amp;hellip; She said &amp;ldquo;I only have
ten or eleven cats, and anyway they are getting quite old now&amp;hellip;&lt;/p>
&lt;/blockquote>
&lt;p>I visually inspected the &amp;ldquo;products&amp;rdquo; table and found the pet food
section. They specifically sell senior cat food!&lt;/p>
&lt;p>The goal is: - find people in Staten Island - find which one buys the
most senior cat food&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Find the staten islanders&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>staten_islanders &lt;span style="color:#f92672">=&lt;/span> customers&lt;span style="color:#f92672">.&lt;/span>filter(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;citystatezip&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>str&lt;span style="color:#f92672">.&lt;/span>starts_with(&lt;span style="color:#e6db74">&amp;#34;Staten Island&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Find the senior cat food items&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>senior_cat_food_products &lt;span style="color:#f92672">=&lt;/span> products&lt;span style="color:#f92672">.&lt;/span>filter(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;desc&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>str&lt;span style="color:#f92672">.&lt;/span>contains(&lt;span style="color:#e6db74">&amp;#34;Senior Cat Food&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Find who has bought the most senior cat food among all customers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>orders_items_with_cat_food &lt;span style="color:#f92672">=&lt;/span> orders_items&lt;span style="color:#f92672">.&lt;/span>join(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> senior_cat_food_products, on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;sku&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>orders_with_cat_food &lt;span style="color:#f92672">=&lt;/span> orders&lt;span style="color:#f92672">.&lt;/span>join(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> orders_items_with_cat_food, on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;orderid&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cat_food_mvp &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> orders_with_cat_food&lt;span style="color:#f92672">.&lt;/span>group_by(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>agg(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;qty&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>sum())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>sort(&lt;span style="color:#e6db74">&amp;#34;qty&amp;#34;&lt;/span>, descending&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>limit(&lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>item()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customers&lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> cat_food_mvp)&lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;phone&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (1, 3)
┌────────────┬───────────────┬──────────────┐
│ customerid ┆ name ┆ phone │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ str │
╞════════════╪═══════════════╪══════════════╡
│ 3068 ┆ Nicole Wilson ┆ 631-507-6048 │
└────────────┴───────────────┴──────────────┘
&lt;/code>&lt;/pre>
&lt;h1 id="part-6-the-bargain-hunter">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/hannukah-of-data/#part-6-the-bargain-hunter">Part 6: The Bargain Hunter&lt;/a>
&lt;/h1>
&lt;blockquote>
&lt;p>She&amp;rsquo;s always been very frugal, and she clips every coupon and shops
every sale at Noah&amp;rsquo;s Market. In fact I like to tease her that Noah
actually loses money whenever she comes in the store.&lt;/p>
&lt;p>Once the subway fare increased, she stopped coming to visit me&amp;hellip; I
hope she remembers to invite me to the family reunion next year.&lt;/p>
&lt;/blockquote>
&lt;p>If the market really does lose money whenever she purchases something,
it means that items she&amp;rsquo;s buying cost less per-unit than they do
wholesale.&lt;/p>
&lt;p>In other words, the market is &lt;strong>selling items at a loss&lt;/strong> to Nicole
Wilson&amp;rsquo;s cousin.&lt;/p>
&lt;h2 id="but-first-whats-a-subway-red-herring">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/hannukah-of-data/#but-first-whats-a-subway-red-herring">But first, what&amp;rsquo;s a subway? (Red herring)&lt;/a>
&lt;/h2>
&lt;p>Can we use the information that her cousin stopped visiting her due to
subway fare increase? This confused me at first, because all of the
trains on Staten Island are aboveground. Aren&amp;rsquo;t subways &lt;em>below ground?&lt;/em>.&lt;/p>
&lt;p>But after poking around online, it seems like the whole system is
referred to as &amp;ldquo;The Subway&amp;rdquo; citywide, regardless if many sections are
aboveground. To get technical, Staten Island Railway is &amp;ldquo;operated by the
New York City Transit Authority &lt;strong>Department of Subways&lt;/strong>&amp;rdquo; and &amp;ldquo;the line
uses modified R44 and R211S &lt;strong>subway cars&lt;/strong>&amp;rdquo; according to &lt;a href="https://en.wikipedia.org/wiki/Staten_Island_Railway">its Wikipedia
page&lt;/a>.&lt;/p>
&lt;p>One important piece of info is that Staten Island is &lt;em>not&lt;/em> connected to
any other part of NYC by rail. So maybe I&amp;rsquo;ll get lucky and find only one
cousin (someone with the same last name as the lady from part 5,
hopefully) who lives on Staten Island.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>print(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customers&lt;span style="color:#f92672">.&lt;/span>filter(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;citystatezip&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>str&lt;span style="color:#f92672">.&lt;/span>starts_with(&lt;span style="color:#e6db74">&amp;#34;Staten Island&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>str&lt;span style="color:#f92672">.&lt;/span>ends_with(&lt;span style="color:#e6db74">&amp;#34;Wilson&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )&lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;phone&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (4, 2)
┌─────────────────┬──────────────┐
│ name ┆ phone │
│ --- ┆ --- │
│ str ┆ str │
╞═════════════════╪══════════════╡
│ Nicole Wilson ┆ 631-507-6048 │
│ Michaela Wilson ┆ 315-859-7694 │
│ Thomas Wilson ┆ 914-910-1529 │
│ Edwin Wilson ┆ 516-767-0295 │
└─────────────────┴──────────────┘
&lt;/code>&lt;/pre>
&lt;p>Alas, none of those people are the answer.
I guess it was just to highlight her cheapness.
It was still a fun detour, though.&lt;/p>
&lt;p>I&amp;rsquo;ll make a table where I compute the profit made on every order.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>orders_items_including_profit &lt;span style="color:#f92672">=&lt;/span> products&lt;span style="color:#f92672">.&lt;/span>join(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> orders_items, on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;sku&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;span style="color:#f92672">.&lt;/span>with_columns(profit&lt;span style="color:#f92672">=&lt;/span>pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;unit_price&amp;#34;&lt;/span>) &lt;span style="color:#f92672">-&lt;/span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;wholesale_cost&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>profit_per_order &lt;span style="color:#f92672">=&lt;/span> orders_items_including_profit&lt;span style="color:#f92672">.&lt;/span>group_by(&lt;span style="color:#e6db74">&amp;#34;orderid&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>agg(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;profit&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>sum()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(profit_per_order)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (213_232, 2)
┌─────────┬────────┐
│ orderid ┆ profit │
│ --- ┆ --- │
│ i64 ┆ f64 │
╞═════════╪════════╡
│ 121807 ┆ 2.68 │
│ 69163 ┆ 0.66 │
│ 111851 ┆ 0.84 │
│ 132326 ┆ 5.29 │
│ 175836 ┆ 5.17 │
│ … ┆ … │
│ 3367 ┆ 0.86 │
│ 179104 ┆ 4.35 │
│ 138848 ┆ 3.85 │
│ 166376 ┆ 0.62 │
│ 162393 ┆ 0.47 │
└─────────┴────────┘
&lt;/code>&lt;/pre>
&lt;p>Then I&amp;rsquo;ll find the customer who has the lowest (since it&amp;rsquo;s negative)
total profit summed across their orders.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Create a table with orderid, customerid, and profit&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>orders_including_profits &lt;span style="color:#f92672">=&lt;/span> orders&lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;orderid&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>join(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> profit_per_order, on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;orderid&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Group by customer id and take the sum of the profit&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> orders_including_profits&lt;span style="color:#f92672">.&lt;/span>group_by(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>agg(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;profit&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>sum())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>sort(&lt;span style="color:#e6db74">&amp;#34;profit&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>head()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (5, 2)
┌────────────┬────────┐
│ customerid ┆ profit │
│ --- ┆ --- │
│ i64 ┆ f64 │
╞════════════╪════════╡
│ 4167 ┆ -85.59 │
│ 8286 ┆ -1.04 │
│ 7676 ┆ -0.17 │
│ 6309 ┆ -0.04 │
│ 2908 ┆ -0.02 │
└────────────┴────────┘
&lt;/code>&lt;/pre>
&lt;p>It seems that customer with ID 4167 has costed Noah&amp;rsquo;s Market $85.59
over the course of their career as a penny-pincher.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>print(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customers&lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">4167&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;phone&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (1, 3)
┌────────────┬─────────────┬──────────────┐
│ customerid ┆ name ┆ phone │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ str │
╞════════════╪═════════════╪══════════════╡
│ 4167 ┆ Sherri Long ┆ 585-838-9161 │
└────────────┴─────────────┴──────────────┘
&lt;/code>&lt;/pre>
&lt;h1 id="part-7-the-meet-cute">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/hannukah-of-data/#part-7-the-meet-cute">Part 7: The Meet Cute&lt;/a>
&lt;/h1>
&lt;blockquote>
&lt;p>I turned around to see this cute guy holding an item I had bought. He
said, &amp;lsquo;I got the same thing!&amp;rsquo; We laughed about it and wound up
swapping items because I wanted the color he got. I asked him to get
some food with me and we spent the rest of the day together.&lt;/p>
&lt;/blockquote>
&lt;p>First, I want to find all the items with colors in their names. After
inspecting the products table, it seems that colors are written in
&lt;strong>lower-case&lt;/strong> at the end of the product description in parentheses.
I&amp;rsquo;ll separate the product color into its own column.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Products with colors in the name always end in e.g. `... (blue)`, so use a regex that pulls that color name out&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>color_name_filter &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">r&lt;/span>&lt;span style="color:#e6db74">&amp;#34;(.+) \(([[:lower:]]+)\)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>colorful_products &lt;span style="color:#f92672">=&lt;/span> products&lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;desc&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>str&lt;span style="color:#f92672">.&lt;/span>contains(color_name_filter))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>products_with_colors_extracted &lt;span style="color:#f92672">=&lt;/span> colorful_products&lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;sku&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;desc&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>str&lt;span style="color:#f92672">.&lt;/span>extract(color_name_filter, group_index&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;desc&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>str&lt;span style="color:#f92672">.&lt;/span>extract(color_name_filter, group_index&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;color&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(products_with_colors_extracted)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (84, 3)
┌─────────┬──────────────────────┬─────────┐
│ sku ┆ desc ┆ color │
│ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str │
╞═════════╪══════════════════════╪═════════╡
│ COL0037 ┆ Noah's Jewelry ┆ green │
│ COL0065 ┆ Noah's Jewelry ┆ mauve │
│ COL0166 ┆ Noah's Action Figure ┆ blue │
│ COL0167 ┆ Noah's Bobblehead ┆ blue │
│ COL0483 ┆ Noah's Action Figure ┆ mauve │
│ … ┆ … ┆ … │
│ COL9349 ┆ Noah's Action Figure ┆ orange │
│ COL9420 ┆ Noah's Jewelry ┆ amber │
│ COL9773 ┆ Noah's Poster ┆ magenta │
│ COL9819 ┆ Noah's Lunchbox ┆ blue │
│ COL9948 ┆ Noah's Jewelry ┆ magenta │
└─────────┴──────────────────────┴─────────┘
&lt;/code>&lt;/pre>
&lt;p>Ok now I&amp;rsquo;ll find all orders with items that contain &amp;ldquo;colorful products&amp;rdquo;.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>orders_with_colorful_products &lt;span style="color:#f92672">=&lt;/span> orders_items&lt;span style="color:#f92672">.&lt;/span>join(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> products_with_colors_extracted, on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;sku&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(orders_with_colorful_products)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (28_012, 6)
┌─────────┬─────────┬─────┬────────────┬──────────────────────┬─────────┐
│ orderid ┆ sku ┆ qty ┆ unit_price ┆ desc ┆ color │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ i64 ┆ f64 ┆ str ┆ str │
╞═════════╪═════════╪═════╪════════════╪══════════════════════╪═════════╡
│ 1014 ┆ COL4117 ┆ 1 ┆ 4.55 ┆ Noah's Poster ┆ yellow │
│ 1015 ┆ COL8357 ┆ 1 ┆ 13.48 ┆ Noah's Lunchbox ┆ mauve │
│ 1018 ┆ COL6388 ┆ 1 ┆ 3.72 ┆ Noah's Gift Box ┆ magenta │
│ 1040 ┆ COL7454 ┆ 1 ┆ 10.65 ┆ Noah's Jersey ┆ mauve │
│ 1041 ┆ COL2141 ┆ 1 ┆ 5.87 ┆ Noah's Bobblehead ┆ puce │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 214217 ┆ COL9349 ┆ 1 ┆ 18.31 ┆ Noah's Action Figure ┆ orange │
│ 214218 ┆ COL0837 ┆ 1 ┆ 4.97 ┆ Noah's Poster ┆ mauve │
│ 214227 ┆ COL1263 ┆ 1 ┆ 21.15 ┆ Noah's Action Figure ┆ yellow │
│ 214231 ┆ COL3430 ┆ 1 ┆ 13.45 ┆ Noah's Lunchbox ┆ green │
│ 214232 ┆ COL6388 ┆ 1 ┆ 4.25 ┆ Noah's Gift Box ┆ magenta │
└─────────┴─────────┴─────┴────────────┴──────────────────────┴─────────┘
&lt;/code>&lt;/pre>
&lt;p>And now I have to find consecutive orders (the orderids auto-increment
by one) which have the same item but a different color&amp;hellip; This is pretty
hard for me conceptually, because I don&amp;rsquo;t know how to compare rows only
if they&amp;rsquo;re sequential.&lt;/p>
&lt;p>Maybe I could do a group_by on &amp;ldquo;desc&amp;rdquo; (which no longer includes color),
collect the orderids into a list, and compare the consecutive orders in
those lists.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>orders_by_colorful_products &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> orders_with_colorful_products&lt;span style="color:#f92672">.&lt;/span>group_by(&lt;span style="color:#e6db74">&amp;#34;desc&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>agg(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;orderid&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>sort()) &lt;span style="color:#75715e"># Must sort orders for pairwise filter later&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>sort(&lt;span style="color:#e6db74">&amp;#34;desc&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(orders_by_colorful_products)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (7, 2)
┌──────────────────────┬────────────────────────┐
│ desc ┆ orderid │
│ --- ┆ --- │
│ str ┆ list[i64] │
╞══════════════════════╪════════════════════════╡
│ Noah's Action Figure ┆ [1073, 1147, … 214227] │
│ Noah's Bobblehead ┆ [1041, 1071, … 214150] │
│ Noah's Gift Box ┆ [1018, 1167, … 214232] │
│ Noah's Jersey ┆ [1040, 1066, … 214184] │
│ Noah's Jewelry ┆ [1053, 1086, … 214202] │
│ Noah's Lunchbox ┆ [1015, 1086, … 214231] │
│ Noah's Poster ┆ [1014, 1047, … 214218] │
└──────────────────────┴────────────────────────┘
&lt;/code>&lt;/pre>
&lt;p>For each product&amp;rsquo;s list of order ids above, I must find all the order
ids that are consecutive. This is called going pairwise through the
list, and luckily python &lt;code>itertools&lt;/code> implements that for me.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> itertools &lt;span style="color:#f92672">import&lt;/span> pairwise
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">filter_consecutive_ids&lt;/span>(series: pl&lt;span style="color:#f92672">.&lt;/span>List(pl&lt;span style="color:#f92672">.&lt;/span>Int64)) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> list[tuple[int, int]]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> output &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> pair &lt;span style="color:#f92672">in&lt;/span> pairwise(series):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> pair[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">-&lt;/span> pair[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> output&lt;span style="color:#f92672">.&lt;/span>append(pair)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> output
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>items_ordered_consecutively &lt;span style="color:#f92672">=&lt;/span> orders_by_colorful_products&lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;desc&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;orderid&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>map_elements(filter_consecutive_ids, return_dtype&lt;span style="color:#f92672">=&lt;/span>pl&lt;span style="color:#f92672">.&lt;/span>List(pl&lt;span style="color:#f92672">.&lt;/span>List(pl&lt;span style="color:#f92672">.&lt;/span>Int64)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;consecutive_orderid&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;span style="color:#f92672">.&lt;/span>explode(&lt;span style="color:#e6db74">&amp;#34;consecutive_orderid&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(items_ordered_consecutively)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (550, 2)
┌──────────────────────┬─────────────────────┐
│ desc ┆ consecutive_orderid │
│ --- ┆ --- │
│ str ┆ list[i64] │
╞══════════════════════╪═════════════════════╡
│ Noah's Action Figure ┆ [2220, 2221] │
│ Noah's Action Figure ┆ [4573, 4574] │
│ Noah's Action Figure ┆ [4946, 4947] │
│ Noah's Action Figure ┆ [11945, 11946] │
│ Noah's Action Figure ┆ [13250, 13251] │
│ … ┆ … │
│ Noah's Poster ┆ [201437, 201438] │
│ Noah's Poster ┆ [202011, 202012] │
│ Noah's Poster ┆ [204001, 204002] │
│ Noah's Poster ┆ [205182, 205183] │
│ Noah's Poster ┆ [210059, 210060] │
└──────────────────────┴─────────────────────┘
&lt;/code>&lt;/pre>
&lt;p>This is a table of all the consecutive orders where a specific item
(ignoring its color) was bought. I now need to filter for the rows where
the color changed between each order in the list.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">get_orderids_to_item_color&lt;/span>(series: pl&lt;span style="color:#f92672">.&lt;/span>List(pl&lt;span style="color:#f92672">.&lt;/span>Int64)) &lt;span style="color:#f92672">-&amp;gt;&lt;/span> pl&lt;span style="color:#f92672">.&lt;/span>List(pl&lt;span style="color:#f92672">.&lt;/span>String):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># orders_with_colorful_products is a global variable :)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> orders_with_colorful_products&lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;orderid&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>is_in(series))&lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;color&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># - Add a &amp;#39;color&amp;#39; column that has the color of each item from the corresponding orderid list&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># - Filter for rows where exactly one item was bought in each order (length 2)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># - Filter for rows where the color list has two unique elements&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>items_ordered_consecutively_with_color &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> items_ordered_consecutively&lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;desc&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;consecutive_orderid&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;consecutive_orderid&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>map_elements(get_orderids_to_item_color, return_dtype&lt;span style="color:#f92672">=&lt;/span>pl&lt;span style="color:#f92672">.&lt;/span>List(pl&lt;span style="color:#f92672">.&lt;/span>String))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;color&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;color&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>list&lt;span style="color:#f92672">.&lt;/span>len() &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;color&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>list&lt;span style="color:#f92672">.&lt;/span>n_unique() &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(items_ordered_consecutively_with_color)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (401, 3)
┌──────────────────────┬─────────────────────┬─────────────────────┐
│ desc ┆ consecutive_orderid ┆ color │
│ --- ┆ --- ┆ --- │
│ str ┆ list[i64] ┆ list[str] │
╞══════════════════════╪═════════════════════╪═════════════════════╡
│ Noah's Action Figure ┆ [2220, 2221] ┆ [&amp;quot;orange&amp;quot;, &amp;quot;amber&amp;quot;] │
│ Noah's Action Figure ┆ [4573, 4574] ┆ [&amp;quot;white&amp;quot;, &amp;quot;purple&amp;quot;] │
│ Noah's Action Figure ┆ [4946, 4947] ┆ [&amp;quot;amber&amp;quot;, &amp;quot;purple&amp;quot;] │
│ Noah's Action Figure ┆ [13250, 13251] ┆ [&amp;quot;azure&amp;quot;, &amp;quot;purple&amp;quot;] │
│ Noah's Action Figure ┆ [13649, 13650] ┆ [&amp;quot;yellow&amp;quot;, &amp;quot;green&amp;quot;] │
│ … ┆ … ┆ … │
│ Noah's Poster ┆ [189701, 189702] ┆ [&amp;quot;blue&amp;quot;, &amp;quot;white&amp;quot;] │
│ Noah's Poster ┆ [197802, 197803] ┆ [&amp;quot;red&amp;quot;, &amp;quot;azure&amp;quot;] │
│ Noah's Poster ┆ [201437, 201438] ┆ [&amp;quot;azure&amp;quot;, &amp;quot;red&amp;quot;] │
│ Noah's Poster ┆ [204001, 204002] ┆ [&amp;quot;green&amp;quot;, &amp;quot;yellow&amp;quot;] │
│ Noah's Poster ┆ [210059, 210060] ┆ [&amp;quot;purple&amp;quot;, &amp;quot;azure&amp;quot;] │
└──────────────────────┴─────────────────────┴─────────────────────┘
&lt;/code>&lt;/pre>
&lt;p>I have the above table with 401 orders that are consecutive and have the
same item in a different color.&lt;/p>
&lt;p>I&amp;rsquo;ll find the rows where either orderid belongs to Sherri Long, the
woman from part 6.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>part6_customerid &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">4167&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>part6_customer_orderids &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> orders&lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> part6_customerid)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;orderid&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>to_series()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>colorful_orders_from_part6_customer &lt;span style="color:#f92672">=&lt;/span> items_ordered_consecutively_with_color&lt;span style="color:#f92672">.&lt;/span>filter(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;consecutive_orderid&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>list&lt;span style="color:#f92672">.&lt;/span>eval(pl&lt;span style="color:#f92672">.&lt;/span>element()&lt;span style="color:#f92672">.&lt;/span>is_in(part6_customer_orderids))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>list&lt;span style="color:#f92672">.&lt;/span>any()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(colorful_orders_from_part6_customer)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (1, 3)
┌───────────────┬─────────────────────┬─────────────────────┐
│ desc ┆ consecutive_orderid ┆ color │
│ --- ┆ --- ┆ --- │
│ str ┆ list[i64] ┆ list[str] │
╞═══════════════╪═════════════════════╪═════════════════════╡
│ Noah's Poster ┆ [70502, 70503] ┆ [&amp;quot;orange&amp;quot;, &amp;quot;azure&amp;quot;] │
└───────────────┴─────────────────────┴─────────────────────┘
&lt;/code>&lt;/pre>
&lt;p>I&amp;rsquo;ll programatically get the order id + customer id of the customer that
is not Sherri Long from the above list of orderids.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>print(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> colorful_orders_from_part6_customer&lt;span style="color:#f92672">.&lt;/span>explode(&lt;span style="color:#e6db74">&amp;#34;consecutive_orderid&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;consecutive_orderid&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>is_in(part6_customer_orderids)&lt;span style="color:#f92672">.&lt;/span>not_())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>join(orders, left_on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;consecutive_orderid&amp;#34;&lt;/span>, right_on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;orderid&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>join(customers, on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;phone&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (1, 3)
┌────────────┬──────────────┬──────────────┐
│ customerid ┆ name ┆ phone │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ str │
╞════════════╪══════════════╪══════════════╡
│ 5783 ┆ Carlos Myers ┆ 838-335-7157 │
└────────────┴──────────────┴──────────────┘
&lt;/code>&lt;/pre>
&lt;h1 id="part-8-the-collector">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/hannukah-of-data/#part-8-the-collector">Part 8: The Collector&lt;/a>
&lt;/h1>
&lt;blockquote>
&lt;p>I gave it to my sister. She wound up getting a newer and more
expensive carpet, so she gave it to an acquaintance of hers who
collects all sorts of junk. Apparently he owns an entire set of Noah&amp;rsquo;s
collectibles!&lt;/p>
&lt;/blockquote>
&lt;p>Giving it to his sister seems like a red herring, and there&amp;rsquo;s not much
info I can glean from that. So instead, I&amp;rsquo;ll focus on the acquaintance.&lt;/p>
&lt;p>First step is to figure out what the collectibles look like in the
database. I learn from visual inspection that collectibles have a &lt;code>sku&lt;/code>
starting with &amp;ldquo;COL&amp;rdquo;.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>collectibles &lt;span style="color:#f92672">=&lt;/span> products&lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;sku&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>str&lt;span style="color:#f92672">.&lt;/span>starts_with(&lt;span style="color:#e6db74">&amp;#34;COL&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(collectibles)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (85, 4)
┌─────────┬───────────────────────────────┬────────────────┬────────────────┐
│ sku ┆ desc ┆ wholesale_cost ┆ dims_cm │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ f64 ┆ str │
╞═════════╪═══════════════════════════════╪════════════════╪════════════════╡
│ COL0037 ┆ Noah's Jewelry (green) ┆ 28.32 ┆ 17.4|11.2|5.7 │
│ COL0041 ┆ Noah's Ark Model (HO Scale) ┆ 2487.35 ┆ 7.2|4.3|0.4 │
│ COL0065 ┆ Noah's Jewelry (mauve) ┆ 33.52 ┆ 19.0|12.2|10.5 │
│ COL0166 ┆ Noah's Action Figure (blue) ┆ 13.98 ┆ 12.1|7.7|7.2 │
│ COL0167 ┆ Noah's Bobblehead (blue) ┆ 5.36 ┆ 8.9|5.6|0.6 │
│ … ┆ … ┆ … ┆ … │
│ COL9349 ┆ Noah's Action Figure (orange) ┆ 15.47 ┆ 16.6|12.9|11.9 │
│ COL9420 ┆ Noah's Jewelry (amber) ┆ 30.01 ┆ 13.8|9.1|4.9 │
│ COL9773 ┆ Noah's Poster (magenta) ┆ 4.13 ┆ 7.2|4.9|1.9 │
│ COL9819 ┆ Noah's Lunchbox (blue) ┆ 9.87 ┆ 14.2|4.1|3.3 │
│ COL9948 ┆ Noah's Jewelry (magenta) ┆ 32.97 ┆ 18.0|17.5|6.2 │
└─────────┴───────────────────────────────┴────────────────┴────────────────┘
&lt;/code>&lt;/pre>
&lt;p>I&amp;rsquo;ll find all the orders that contain any collectibles.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>orders_items_collectibles &lt;span style="color:#f92672">=&lt;/span> orders_items&lt;span style="color:#f92672">.&lt;/span>join(collectibles, on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;sku&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(orders_items_collectibles)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (28_013, 7)
┌─────────┬─────────┬─────┬────────────┬─────────────────────────┬────────────────┬────────────────┐
│ orderid ┆ sku ┆ qty ┆ unit_price ┆ desc ┆ wholesale_cost ┆ dims_cm │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ i64 ┆ f64 ┆ str ┆ f64 ┆ str │
╞═════════╪═════════╪═════╪════════════╪═════════════════════════╪════════════════╪════════════════╡
│ 1014 ┆ COL4117 ┆ 1 ┆ 4.55 ┆ Noah's Poster (yellow) ┆ 3.63 ┆ 19.7|12.0|6.3 │
│ 1015 ┆ COL8357 ┆ 1 ┆ 13.48 ┆ Noah's Lunchbox (mauve) ┆ 9.21 ┆ 19.9|17.4|9.8 │
│ 1018 ┆ COL6388 ┆ 1 ┆ 3.72 ┆ Noah's Gift Box ┆ 3.28 ┆ 19.0|9.5|2.2 │
│ ┆ ┆ ┆ ┆ (magenta) ┆ ┆ │
│ 1040 ┆ COL7454 ┆ 1 ┆ 10.65 ┆ Noah's Jersey (mauve) ┆ 8.19 ┆ 19.1|18.2|3.5 │
│ 1041 ┆ COL2141 ┆ 1 ┆ 5.87 ┆ Noah's Bobblehead ┆ 4.85 ┆ 19.0|10.4|10.2 │
│ ┆ ┆ ┆ ┆ (puce) ┆ ┆ │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 214217 ┆ COL9349 ┆ 1 ┆ 18.31 ┆ Noah's Action Figure ┆ 15.47 ┆ 16.6|12.9|11.9 │
│ ┆ ┆ ┆ ┆ (orange) ┆ ┆ │
│ 214218 ┆ COL0837 ┆ 1 ┆ 4.97 ┆ Noah's Poster (mauve) ┆ 3.86 ┆ 13.0|3.3|0.8 │
│ 214227 ┆ COL1263 ┆ 1 ┆ 21.15 ┆ Noah's Action Figure ┆ 16.5 ┆ 10.2|7.5|3.6 │
│ ┆ ┆ ┆ ┆ (yellow) ┆ ┆ │
│ 214231 ┆ COL3430 ┆ 1 ┆ 13.45 ┆ Noah's Lunchbox (green) ┆ 10.22 ┆ 16.0|6.9|1.9 │
│ 214232 ┆ COL6388 ┆ 1 ┆ 4.25 ┆ Noah's Gift Box ┆ 3.28 ┆ 19.0|9.5|2.2 │
│ ┆ ┆ ┆ ┆ (magenta) ┆ ┆ │
└─────────┴─────────┴─────┴────────────┴─────────────────────────┴────────────────┴────────────────┘
&lt;/code>&lt;/pre>
&lt;p>Now I&amp;rsquo;ll get the corresponding customerid for each of those rows.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>customers_who_bought_collectibles &lt;span style="color:#f92672">=&lt;/span> orders_items_collectibles&lt;span style="color:#f92672">.&lt;/span>join(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> orders, on&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;orderid&amp;#34;&lt;/span>, how&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;inner&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(customers_who_bought_collectibles&lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>))&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (28_013, 1)
┌────────────┐
│ customerid │
│ --- │
│ i64 │
╞════════════╡
│ 4716 │
│ 3808 │
│ 2645 │
│ 2520 │
│ 8385 │
│ … │
│ 6222 │
│ 5950 │
│ 8352 │
│ 3894 │
│ 1368 │
└────────────┘
&lt;/code>&lt;/pre>
&lt;p>Group by customerid and get a list of the collectibles they bought.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>customers_bought_num_collectibles &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customers_who_bought_collectibles&lt;span style="color:#f92672">.&lt;/span>group_by(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>agg(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;sku&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>with_columns(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;sku&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>list&lt;span style="color:#f92672">.&lt;/span>len()&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;num_collectibles&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>sort(&lt;span style="color:#e6db74">&amp;#34;num_collectibles&amp;#34;&lt;/span>, descending&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(customers_bought_num_collectibles&lt;span style="color:#f92672">.&lt;/span>head())&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (5, 3)
┌────────────┬─────────────────────────────────┬──────────────────┐
│ customerid ┆ sku ┆ num_collectibles │
│ --- ┆ --- ┆ --- │
│ i64 ┆ list[str] ┆ u32 │
╞════════════╪═════════════════════════════════╪══════════════════╡
│ 3808 ┆ [&amp;quot;COL8357&amp;quot;, &amp;quot;COL6858&amp;quot;, … &amp;quot;COL2… ┆ 111 │
│ 1787 ┆ [&amp;quot;COL6461&amp;quot;, &amp;quot;COL1263&amp;quot;, … &amp;quot;COL9… ┆ 37 │
│ 6855 ┆ [&amp;quot;COL8354&amp;quot;, &amp;quot;COL9011&amp;quot;, … &amp;quot;COL5… ┆ 36 │
│ 3580 ┆ [&amp;quot;COL4363&amp;quot;, &amp;quot;COL5018&amp;quot;, … &amp;quot;COL2… ┆ 36 │
│ 8352 ┆ [&amp;quot;COL9948&amp;quot;, &amp;quot;COL9349&amp;quot;, … &amp;quot;COL1… ┆ 34 │
└────────────┴─────────────────────────────────┴──────────────────┘
&lt;/code>&lt;/pre>
&lt;p>Customer with id 3808 has bought the most collectibles by far.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>print(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customers&lt;span style="color:#f92672">.&lt;/span>filter(pl&lt;span style="color:#f92672">.&lt;/span>col(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">3808&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;customerid&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;phone&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;pre>&lt;code>shape: (1, 3)
┌────────────┬─────────────┬──────────────┐
│ customerid ┆ name ┆ phone │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ str │
╞════════════╪═════════════╪══════════════╡
│ 3808 ┆ James Smith ┆ 212-547-3518 │
└────────────┴─────────────┴──────────────┘
&lt;/code>&lt;/pre>
&lt;h1 id="conclusion">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/hannukah-of-data/#conclusion">Conclusion&lt;/a>
&lt;/h1>
&lt;p>Once I finished these puzzles, there was a speedrun that was unlocked which had larger datasets and slightly different questions.
Luckily my solutions were correct enough to only require minor tweaking to finish the speedrun and I did it in 14 minutes.&lt;/p>
&lt;p>I had a lot of fun figuring out these puzzles, and I definitely improved my data analysis skills.&lt;/p>
&lt;p>Major props to the creators of this game, &amp;ldquo;the &lt;a href="https://www.bluebird.sh/">devottys&lt;/a>&amp;rdquo;.
It was well made, required careful analysis of the data and even the puzzle wording itself.&lt;/p>
&lt;p>They also included amazing ascii art which reveals itself to be Noah&amp;rsquo;s Rug hung over a fully lit menorah at the end.&lt;/p>
&lt;figure>&lt;img src="https://ajcajc.com/noahs-tapestry-speedrun-finished.png"
alt="ASCII art of Noah&amp;#39;s Rug, a beautiful tapestry that includes all kinds of animals and insects, hung over a fully lit menorah">
&lt;/figure></description></item><item><title>How is location shared without using GPS?</title><link>https://ajcajc.com/investigating-browser-geolocation/</link><pubDate>Fri, 11 Oct 2024 00:00:00 +0000</pubDate><guid>https://ajcajc.com/investigating-browser-geolocation/</guid><description>&lt;figure>&lt;img src="https://ajcajc.com/allow-access-question.png"
alt="Browser pop-up asking if I&amp;#39;d like to allow google.com to access my location">
&lt;/figure>
&lt;p>How does location sharing work when my computer doesn&amp;rsquo;t have GPS?&lt;/p>
&lt;p>If I open my laptop to Google Maps and allow it to access my location, it returns my location to within 20 meters!
How can that possibly work without GPS?&lt;/p>
&lt;p>Clicking &lt;a href="https://www.mozilla.org/en-US/firefox/geolocation/">Learn more&lt;/a> in the above pop-up tells me (emphasis mine):&lt;/p>
&lt;blockquote>
&lt;p>By default, Firefox uses Google Location Services to determine your location by sending:&lt;/p>
&lt;/blockquote>
&lt;ul>
&lt;li>your computer’s &lt;strong>IP address&lt;/strong>,&lt;/li>
&lt;li>information about the &lt;strong>nearby wireless access points&lt;/strong>, and&lt;/li>
&lt;/ul>
&lt;h2 id="my-computers-ip-address">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/investigating-browser-geolocation/#my-computers-ip-address">My computer&amp;rsquo;s IP address&lt;/a>
&lt;/h2>
&lt;p>Using IP addresses for geolocation is normally accurate to within the same city, you can read more about &lt;a href="https://whatismyipaddress.com/geolocation">IP address geolocation here&lt;/a>.&lt;/p>
&lt;p>You can look up your own IP address on, for example, &lt;a href="https://www.iplocation.net/ip-lookup">IPLocation.net&lt;/a>, which gives you results from multiple services.
If you look up your IP address, you may notice that not all the results are accurate, so obviously this wouldn&amp;rsquo;t work well as a method for pinpointing your precise location.&lt;/p>
&lt;h2 id="nearby-wireless-access-points">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/investigating-browser-geolocation/#nearby-wireless-access-points">Nearby wireless access points&lt;/a>
&lt;/h2>
&lt;p>This refers to the various WiFi networks that my computer can see.
So high accuracy geolocation comes from WiFi!&lt;/p>
&lt;p>&lt;strong>How does Google determine location from WiFi??&lt;/strong>&lt;/p>
&lt;p>Google has been collecting data on WiFi networks for years.
It &lt;a href="http://static.googleusercontent.com/media/www.google.com/en//googleblogs/pdfs/google_submission_dpas_wifi_collection.pdf">stated back in 2010&lt;/a> that it uses Google Street View cars to collect WiFi data in our neighborhoods.
They store the WiFi networks they detect + the car’s GPS coordinates for future use.&lt;/p>
&lt;figure>&lt;img src="https://ajcajc.com/google-street-view-car.webp"
alt="A parked street view car with its massive camera strapped to the roof">&lt;figcaption>
&lt;h4>Peeping on your WiFi&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;p>So when Firefox sends Google my nearby WiFi networks, Google looks up where it last saw that combination of WiFi networks (which was in my neighborhood) and replies with the coordinates it stored.&lt;/p>
&lt;h1 id="inspecting-the-data-that-firefox-is-sending-to-google">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/investigating-browser-geolocation/#inspecting-the-data-that-firefox-is-sending-to-google">Inspecting the data that Firefox is sending to Google&lt;/a>
&lt;/h1>
&lt;h2 id="using-developer-tools">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/investigating-browser-geolocation/#using-developer-tools">Using Developer Tools&lt;/a>
&lt;/h2>
&lt;p>I want to see if I can intercept the request that Firefox sends to Google.
So I set up a basic webpage that uses Javascript to request the user’s location from the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API">Geolocation API&lt;/a> and display it on a map.&lt;/p>
&lt;figure>&lt;img src="https://ajcajc.com/dev-tools-screenshot.png"
alt="Developer Tools screenshot showing no http requests to any API after clicking &amp;#39;Allow&amp;#39; on the Location Services pop-up">&lt;figcaption>
&lt;h4>Where are my geolocation requests?&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;p>Ok, so it turns out that Firefox doesn&amp;rsquo;t record those geolocation API requests in the developer tools.&lt;/p>
&lt;h2 id="using-an-https-proxy">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/investigating-browser-geolocation/#using-an-https-proxy">Using an HTTP(s) Proxy&lt;/a>
&lt;/h2>
&lt;p>I set up &lt;a href="https://mitmproxy.org/">mitmproxy&lt;/a> to intercept the http(s) requests that Firefox was sending.
This is configurable from within Firefox, so we have to trust Firefox to send all of its requests through the proxy.&lt;/p>
&lt;figure>&lt;img src="https://ajcajc.com/mitm-proxy-screenshot.png"
alt="List of http requests that mitmproxy captured from my session with the map page">
&lt;/figure>
&lt;p>Still, no requests to Google’s API show up after clicking “Allow” on the location popup.
Only requests to my site, map providers, and general Firefox telemetry show up.
Does Firefox not respect the http(s) proxy that it’s configured to use?
Wouldn’t it break in corporate environments where the app must use the proxy?&lt;/p>
&lt;h2 id="using-wireshark">
&lt;a class="Heading-link u-clickable" href="https://ajcajc.com/investigating-browser-geolocation/#using-wireshark">Using Wireshark&lt;/a>
&lt;/h2>
&lt;p>I recently learned how to enable Wireshark &lt;a href="https://wiki.wireshark.org/TLS#tls-dissection-in-wireshark">to decrypt TLS traffic&lt;/a>.
I prefer to do it the easy way by setting SSLKEYLOGFILE when opening Firefox.
I recommend creating a new &lt;a href="https://support.mozilla.org/en-US/kb/profile-manager-create-remove-switch-firefox-profiles">Firefox profile&lt;/a> if you&amp;rsquo;re going to try this, you don&amp;rsquo;t want to accidentally capture your own sensitive data.&lt;/p>
&lt;p>I launch a new browser window with the separate profile and &lt;code>SSLKEYLOGFILE&lt;/code> environment variable set&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>SSLKEYLOGFILE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>$HOME&lt;span style="color:#e6db74">/decrypted.log&amp;#34;&lt;/span> firefox -P &lt;span style="color:#e6db74">&amp;#34;Decrypted&amp;#34;&lt;/span> &amp;amp;&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>Then I visit my map, start the capture, click the &lt;em>Allow&lt;/em> button for the pop-up, wait until it shows my location, stop the capture and &lt;strong>VOILA&lt;/strong>!&lt;/p>
&lt;figure>&lt;img src="https://ajcajc.com/wireshark-geolocate-decrypted-1.png"
alt="Wireshark session showing the decrypted http2 geolocation request">
&lt;/figure>
&lt;p>There&amp;rsquo;s the geolocation POST request with all of my nearby WiFi networks!
But why is it going to&amp;hellip; &lt;code>location.services.mozilla.com&lt;/code>??&lt;/p>
&lt;figure>&lt;img src="https://ajcajc.com/geolocate-post-request-redacted.png"
alt="POST request with all of my nearby access point MAC addresses in the body">
&lt;/figure>
&lt;p>What&amp;rsquo;s more confusing, it failed this time with 404 NOT FOUND.&lt;/p>
&lt;figure>&lt;img src="https://ajcajc.com/geolocate-response-404.png"
alt="POST response saying 404 not found">
&lt;/figure>
&lt;p>I reopened the webpage while running Wireshark, and this time it successfully showed my location.
However, there&amp;rsquo;s no corresponding HTTP request&amp;hellip;
A bit more investigation shows that it&amp;rsquo;s an HTTP3 request, which I know very little about.&lt;/p>
&lt;figure>&lt;img src="https://ajcajc.com/geolocate-http3-request.png"
alt="Wireshark screenshot showing HTTP/3 packets">
&lt;/figure>
&lt;p>Wireshark doesn&amp;rsquo;t show me any interesting info in the HTTP/3 packets, sadly, because it turns out that Wireshark &lt;a href="https://gitlab.com/wireshark/wireshark/-/issues/16761">still hasn&amp;rsquo;t implemented HTTP/3 support&lt;/a>.
I&amp;rsquo;m looking forward to the day that this feature gets released!&lt;/p>
&lt;p>I had fun learning about how geolocation works using WiFi.
It was also satisfying to learn new tools like mitmproxy and learning how to decrypt TLS with Wireshark!&lt;/p></description></item></channel></rss>