<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: mbadr3227-sys</title>
    <description>The latest articles on DEV Community by mbadr3227-sys (@mbadr3227sys).</description>
    <link>https://dev.to/mbadr3227sys</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4065395%2Fb9c88a54-655a-4f03-a053-b2fc5a6f37de.png</url>
      <title>DEV Community: mbadr3227-sys</title>
      <link>https://dev.to/mbadr3227sys</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mbadr3227sys"/>
    <language>en</language>
    <item>
      <title>I built a content recommender as a graph, and it found similarities I wasn't looking for</title>
      <dc:creator>mbadr3227-sys</dc:creator>
      <pubDate>Sun, 16 Aug 2026 23:01:05 +0000</pubDate>
      <link>https://dev.to/mbadr3227sys/i-built-a-content-recommender-as-a-graph-and-it-found-similarities-i-wasnt-looking-for-3kj7</link>
      <guid>https://dev.to/mbadr3227sys/i-built-a-content-recommender-as-a-graph-and-it-found-similarities-i-wasnt-looking-for-3kj7</guid>
      <description>&lt;p&gt;Most beginner recommendation projects are a dictionary lookup. You type&lt;br&gt;
"comedy", it returns the comedy list. That works, and it is also the&lt;br&gt;
reason those projects are forgettable: a dictionary can tell you what is&lt;br&gt;
similar, but it can never tell you what is &lt;em&gt;interestingly different&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I wanted the second thing. So I modelled the catalogue as a weighted&lt;br&gt;
graph instead, and ended up finding a kind of similarity I had not&lt;br&gt;
designed for.&lt;/p&gt;

&lt;p&gt;The project is called CreatorRoute. It recommends short-form content&lt;br&gt;
formats: you name a video you liked, and it gives you close matches plus&lt;br&gt;
a few deliberate outliers.&lt;/p&gt;
&lt;h2&gt;
  
  
  The data
&lt;/h2&gt;

&lt;p&gt;Forty rows of short-form videos, hand-written, five attributes each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;niche&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;hook&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;editing&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;cta&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;
&lt;span class="mf"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Stop&lt;/span&gt; &lt;span class="k"&gt;posting&lt;/span&gt; &lt;span class="k"&gt;at&lt;/span&gt; &lt;span class="mf"&gt;9&lt;/span&gt;&lt;span class="k"&gt;am&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;contrarian&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;short&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;jump&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;cut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;comment&lt;/span&gt;
&lt;span class="mf"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Stop&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="k"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;editing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;contrarian&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;short&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;jump&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;cut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;comment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember those two rows. They matter later.&lt;/p&gt;

&lt;p&gt;The one rule I followed while writing the dataset: every attribute value&lt;br&gt;
had to repeat across several rows. Unique values produce isolated nodes,&lt;br&gt;
and isolated nodes make a graph that cannot be traversed. If every video&lt;br&gt;
had its own one-off &lt;code&gt;hook_type&lt;/code&gt;, there would be no edges to walk.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building the graph
&lt;/h2&gt;

&lt;p&gt;Every video is a node. Two nodes get an edge when they share at least&lt;br&gt;
one attribute. The edge weight is the inverse of the number of shared&lt;br&gt;
attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ATTRIBUTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;niche&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hook_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;editing_style&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cta_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shared_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ATTRIBUTES&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_graph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
            &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shared_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;
                &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inverting the count is the whole trick. Four shared attributes gives a&lt;br&gt;
weight of 0.25; one shared attribute gives 1.0. More in common means a&lt;br&gt;
shorter edge, which means shortest-path algorithms rank by similarity&lt;br&gt;
without any extra work.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;range(i + 1, ...)&lt;/code&gt; avoids comparing each pair twice.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where BFS failed
&lt;/h2&gt;

&lt;p&gt;My first plan was breadth-first search alone. Depth 1 for close matches,&lt;br&gt;
depth 2 for discovery. Clean, simple, no weights needed.&lt;/p&gt;

&lt;p&gt;It did not work. At 15 items, a BFS from any node reached &lt;strong&gt;10 of the 14&lt;br&gt;
others at depth 1&lt;/strong&gt;. One shared attribute is enough for an edge, and with&lt;br&gt;
five attributes across a small catalogue, nearly everything connects to&lt;br&gt;
nearly everything.&lt;/p&gt;

&lt;p&gt;BFS answers &lt;em&gt;is this reachable, and in how many hops&lt;/em&gt;. In a dense graph,&lt;br&gt;
the answer is almost always "yes, one hop", which is not a ranking. A&lt;br&gt;
video sharing four attributes and a video sharing one were both simply&lt;br&gt;
"neighbours".&lt;/p&gt;
&lt;h2&gt;
  
  
  Where Dijkstra fixed it
&lt;/h2&gt;

&lt;p&gt;Dijkstra's algorithm walks the same graph but accumulates weight instead&lt;br&gt;
of counting hops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;heapq&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dijkstra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;distances&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;heap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;settled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;heap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;heapq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;heappop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;heap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;settled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;settled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;neighbor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;new_dist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;new_dist&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;distances&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;neighbor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
                &lt;span class="n"&gt;distances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;neighbor&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_dist&lt;/span&gt;
                &lt;span class="n"&gt;heapq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;heappush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;heap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_dist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;neighbor&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;distances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;distances&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same neighbours, real ordering. Something four-fifths identical scores&lt;br&gt;
0.25 and something barely related scores 1.0.&lt;/p&gt;

&lt;p&gt;BFS was not wasted, though. It moved jobs: it now supplies the discovery&lt;br&gt;
list, pulling nodes at depth 2 that share nothing directly with your pick&lt;br&gt;
but sit two hops away. Two algorithms, two questions, one graph.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part I did not plan
&lt;/h2&gt;

&lt;p&gt;Here is the output for "Stop posting at 9am", a &lt;strong&gt;marketing&lt;/strong&gt; video:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Because you liked: Stop posting at 9am
(marketing / contrarian / jump-cut)

  Closest matches:
    - 3 mistakes killing your ad spend  [0.25]
    - The one word killing your CTA     [0.25]
    - Stop using this transition        [0.25]

  Worth exploring:
    - 6 free tools I use daily
    - 5 onboarding flows that work
    - My first 1000 orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third close match is about video editing. The second is about&lt;br&gt;
copywriting. Neither is marketing.&lt;/p&gt;

&lt;p&gt;They rank at the top because they share &lt;code&gt;contrarian&lt;/code&gt; + &lt;code&gt;short&lt;/code&gt; +&lt;br&gt;
&lt;code&gt;jump-cut&lt;/code&gt; + &lt;code&gt;comment&lt;/code&gt;. The graph had quietly learned to match on&lt;br&gt;
&lt;strong&gt;format&lt;/strong&gt; rather than &lt;strong&gt;topic&lt;/strong&gt; — same shape of video, different&lt;br&gt;
subject entirely.&lt;/p&gt;

&lt;p&gt;I did not build that. I built "count shared attributes". Treating topic&lt;br&gt;
as one attribute among five, rather than as the primary key, was enough&lt;br&gt;
for structural similarity to emerge on its own. A dictionary keyed on&lt;br&gt;
category could not have surfaced it, because the key would have thrown&lt;br&gt;
the other four attributes away before the comparison started.&lt;/p&gt;
&lt;h2&gt;
  
  
  Search has the same trade-off
&lt;/h2&gt;

&lt;p&gt;The CLI needs to find your video before it can recommend anything, and&lt;br&gt;
that turned out to be its own small lesson in complexity.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;prefix_search&lt;/code&gt; uses binary search over a sorted index — O(log n), but&lt;br&gt;
only matches titles that &lt;em&gt;start&lt;/em&gt; with your query. &lt;code&gt;keyword_search&lt;/code&gt; scans&lt;br&gt;
every title at O(n) and matches anywhere. Searching "ad" finds nothing&lt;br&gt;
with the fast one and four titles with the slow one, including&lt;br&gt;
"Rewriting a bad ad live".&lt;/p&gt;

&lt;p&gt;The CLI tries fast first and falls back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;prefix_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;keyword_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 40 items the difference is unmeasurable. Writing both anyway made the&lt;br&gt;
trade-off concrete in a way that reading the Big O table never did.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would change
&lt;/h2&gt;

&lt;p&gt;The dataset is hand-written, which caps how much the graph can surprise&lt;br&gt;
me — I chose the attributes, so I partly chose the connections. Real&lt;br&gt;
scraped data would be a better test.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;build_graph&lt;/code&gt; function is O(n²): every pair compared. Fine at 40&lt;br&gt;
items, painful at 40,000. The fix is bucketing by attribute value and&lt;br&gt;
only comparing within buckets, which I have not needed yet.&lt;/p&gt;

&lt;p&gt;And attributes are currently equal. Sharing an &lt;code&gt;editing_style&lt;/code&gt; counts as&lt;br&gt;
much as sharing a &lt;code&gt;niche&lt;/code&gt;, which is probably wrong. Weighting them&lt;br&gt;
differently is the obvious next experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;If you are working through a recommendation project, resist the&lt;br&gt;
dictionary. The graph is not much more code — under 200 lines total, no&lt;br&gt;
dependencies outside the standard library — and it gives you somewhere&lt;br&gt;
to put a second algorithm, a real reason to care about edge weights, and&lt;br&gt;
occasionally a result you did not design.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/mbadr3227-sys/creatorroute" rel="noopener noreferrer"&gt;github.com/mbadr3227-sys/creatorroute&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>algorithms</category>
      <category>beginners</category>
      <category>datastructures</category>
    </item>
    <item>
      <title>I Kept Rewriting the Same Five Lines of Pandas, So I Built a Terminal Tool Instead</title>
      <dc:creator>mbadr3227-sys</dc:creator>
      <pubDate>Thu, 06 Aug 2026 07:49:34 +0000</pubDate>
      <link>https://dev.to/mbadr3227sys/i-kept-rewriting-the-same-five-lines-of-pandas-so-i-built-a-terminal-tool-instead-44aa</link>
      <guid>https://dev.to/mbadr3227sys/i-kept-rewriting-the-same-five-lines-of-pandas-so-i-built-a-terminal-tool-instead-44aa</guid>
      <description>&lt;p&gt;Every time I opened a new dataset, I typed the same thing:&lt;/p&gt;

&lt;p&gt;df.shape&lt;br&gt;
df.head()&lt;br&gt;
df.isna().sum()&lt;br&gt;
df.describe()&lt;br&gt;
df.corr()&lt;/p&gt;

&lt;p&gt;Five lines, every single time, in a fresh notebook cell. It took maybe forty seconds. But it was forty seconds of typing something I already knew I wanted, before I could start on the part I actually cared about.&lt;/p&gt;

&lt;p&gt;So I built DataPeek — a terminal program that does all of it from a menu. You point it at a CSV, press a number, and read the answer.&lt;/p&gt;

&lt;p&gt;This was my portfolio project for Codecademy's CS 101: Introduction to Programming, and I want to write about the parts that surprised me, not just what the tool does.&lt;br&gt;
The shape of the thing&lt;/p&gt;

&lt;p&gt;The whole program is two files.&lt;/p&gt;

&lt;p&gt;main.py runs the interface: it prints a banner, loads a file, shows a menu, and loops until you quit. data_tools.py holds the analysis — one function per menu option, each taking a DataFrame and printing a report.&lt;/p&gt;

&lt;p&gt;The piece that ties them together is a dictionary:&lt;br&gt;
ACTIONS = {&lt;br&gt;
    "1": dt.show_overview,&lt;br&gt;
    "2": dt.show_head,&lt;br&gt;
    "3": dt.show_missing,&lt;br&gt;
    "4": dt.show_stats,&lt;br&gt;
    "5": dt.show_value_counts,&lt;br&gt;
    "6": dt.show_correlations,&lt;br&gt;
    "7": dt.filter_and_export,&lt;br&gt;
}&lt;br&gt;
My first version was a long if/elif chain. It worked, but every new feature meant editing the loop, and the loop kept growing. Storing the functions in a dictionary meant the loop stopped changing entirely:&lt;br&gt;
action = ACTIONS.get(choice)&lt;br&gt;
if action is None:&lt;br&gt;
    print("I don't know that option. Try a number from the menu, or 'q'.")&lt;br&gt;
    continue&lt;br&gt;
action(df)&lt;br&gt;
Adding a feature is now a function plus one line in the dictionary. That was the first moment the project taught me something I hadn't gone looking for: functions are values you can store, not just things you call.&lt;/p&gt;

&lt;p&gt;Users type whatever they want&lt;/p&gt;

&lt;p&gt;I had never written a program that asked for input before. Every prior exercise ran on data I controlled — a CSV I'd already inspected, a variable I'd set myself two lines earlier.&lt;/p&gt;

&lt;p&gt;The first time I ran DataPeek, I broke it within thirty seconds. I typed python main.py at the prompt — the program was already running, and it was asking me for a file path, but my hands typed the command anyway out of habit. Then I pressed Enter on an empty prompt to see what would happen.&lt;/p&gt;

&lt;p&gt;Both times, it held. It told me it needed a file path and asked again.&lt;/p&gt;

&lt;p&gt;That only worked because I'd already spent most of my later commits writing guards instead of features. The number prompt became this:&lt;br&gt;
def ask_int(prompt, default, low, high):&lt;br&gt;
    raw = input(prompt).strip()&lt;br&gt;
    if not raw:&lt;br&gt;
        return default&lt;br&gt;
    try:&lt;br&gt;
        value = int(raw)&lt;br&gt;
    except ValueError:&lt;br&gt;
        print(f"Not a number. Using {default}.")&lt;br&gt;
        return default&lt;br&gt;
    if not low &amp;lt;= value &amp;lt;= high:&lt;br&gt;
        print(f"Out of range. Using {default}.")&lt;br&gt;
        return default&lt;br&gt;
    return value&lt;br&gt;
Eleven lines to read one number. It felt like overkill when I wrote it, and correct the first time the program survived something I hadn't planned for.&lt;/p&gt;

&lt;p&gt;File loading got the same treatment — separate except blocks for a missing file, an empty file, a malformed CSV, and a permissions error, each with a message that says what to do next. pd.read_csv raises genuinely different exceptions for these, and catching them separately means the user gets "That doesn't look like a valid CSV" instead of a stack trace.&lt;/p&gt;

&lt;p&gt;The general lesson: input validation isn't a finishing touch. It's most of the work of making a program usable by anyone but you.&lt;/p&gt;

&lt;p&gt;Making a terminal readable&lt;/p&gt;

&lt;p&gt;Terminal output has no styling. No bold, no colour, no layout. All you have is spaces and newlines, and it turns out that's enough if you're deliberate.&lt;/p&gt;

&lt;p&gt;f-string alignment did most of the work:&lt;br&gt;
print(f"{col[:24]:&amp;lt;25}{count:&amp;gt;10,}{pct:&amp;gt;9.1f}%")&lt;br&gt;
Left-align the label, right-align the numbers, truncate anything too long, and the columns line up. Right-aligned numbers are easier to compare because the digits stack.&lt;/p&gt;

&lt;p&gt;For missing values I added a crude bar:&lt;/p&gt;

&lt;p&gt;bar = "#" * int(pct / 5)&lt;/p&gt;

&lt;p&gt;One # per five percent. It's about as simple as a visualisation gets, and it still beats reading percentages down a column — you see the worst offenders without comparing anything.&lt;/p&gt;

&lt;p&gt;The part that took longest wasn't the code&lt;/p&gt;

&lt;p&gt;I want to be honest about the time breakdown, because the tutorials never are.&lt;/p&gt;

&lt;p&gt;Writing the program took an afternoon. Getting it to run took considerably longer, and none of that time was spent on Python.&lt;/p&gt;

&lt;p&gt;PowerShell refused to activate the virtual environment — Windows blocks script execution by default, and the fix is a single Set-ExecutionPolicy command that I only found by reading the error message properly instead of panicking at the red text. Then pip install pandas died partway through with [Errno 28] No space left on device. My drive had 130 MB free out of 75 GB. Clearing temporary files bought me a gigabyte, which was just enough.&lt;/p&gt;

&lt;p&gt;Then Git wasn't installed. Then I ran git remote add pointing at a GitHub repository I hadn't created yet, and spent a minute confused by Repository not found before realising the repository genuinely did not exist.&lt;/p&gt;

&lt;p&gt;None of these are interesting problems. All of them are real ones, and they're the actual content of "build a project on your own machine" — the step that tutorials compress into a single line that reads set up your environment.&lt;/p&gt;

&lt;p&gt;Where the correlation code got interesting&lt;/p&gt;

&lt;p&gt;df.corr() gives a full matrix. Every pair appears twice, and the diagonal is all 1.0. Useful to look at, annoying to rank.&lt;/p&gt;

&lt;p&gt;To get a sorted list of distinct pairs, I walked only the upper triangle:&lt;/p&gt;

&lt;p&gt;for i, col_a in enumerate(corr.columns):&lt;br&gt;
    for col_b in corr.columns[i + 1:]:&lt;br&gt;
        value = corr.loc[col_a, col_b]&lt;br&gt;
        if pd.notna(value):&lt;br&gt;
            pairs.append((col_a, col_b, value))&lt;/p&gt;

&lt;p&gt;pairs.sort(key=lambda pair: abs(pair[2]), reverse=True)&lt;/p&gt;

&lt;p&gt;Slicing from i + 1 skips the diagonal and everything below it. Sorting on abs() means a correlation of -0.8 ranks above +0.3, which is what you want — strength matters more than direction when you're scanning for what to look at next.&lt;/p&gt;

&lt;p&gt;What I'd do next&lt;br&gt;
ASCII histograms, so numeric distributions are visible not just summarised&lt;br&gt;
Group-by summaries, since that's the next thing I always reach for&lt;br&gt;
Saving a full report to a text file&lt;br&gt;
What it actually taught me&lt;/p&gt;

&lt;p&gt;I expected to learn pandas. I already knew pandas. What I learned was that the distance between code that works and code someone else can use is mostly made of error handling, clear messages, and structure that lets you add things without breaking what's there — plus a surprising amount of getting a machine to cooperate before any of it runs at all.&lt;/p&gt;

&lt;p&gt;The code is on GitHub: github.com/mbadr3227-sys/datapeek&lt;/p&gt;

&lt;p&gt;If you also keep typing the same five lines, feel free to steal it.&lt;/p&gt;

&lt;p&gt;Content&lt;/p&gt;

</description>
      <category>python</category>
      <category>beginners</category>
      <category>datascience</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
