<?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: 1121 Harris</title>
    <description>The latest articles on DEV Community by 1121 Harris (@1121_harris).</description>
    <link>https://dev.to/1121_harris</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%2F4093831%2F7347eb11-d56e-425f-a3ef-e42a9417fcea.png</url>
      <title>DEV Community: 1121 Harris</title>
      <link>https://dev.to/1121_harris</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/1121_harris"/>
    <language>en</language>
    <item>
      <title>I Turned 200+ Chinese Character Confusions Into a Structured Dataset</title>
      <dc:creator>1121 Harris</dc:creator>
      <pubDate>Tue, 25 Aug 2026 10:07:05 +0000</pubDate>
      <link>https://dev.to/1121_harris/i-turned-200-chinese-character-confusions-into-structured-data-heres-what-i-learned-20l7</link>
      <guid>https://dev.to/1121_harris/i-turned-200-chinese-character-confusions-into-structured-data-heres-what-i-learned-20l7</guid>
      <description>&lt;p&gt;I'm building a Chinese learning project called &lt;a href="https://hanzihero.app" rel="noopener noreferrer"&gt;HanziHero&lt;/a&gt;, and one feature started with what looked like a ridiculously simple data problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why do learners keep mixing up Chinese characters they already know?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Think:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;未 / 末&lt;/li&gt;
&lt;li&gt;土 / 士&lt;/li&gt;
&lt;li&gt;牛 / 午&lt;/li&gt;
&lt;li&gt;日 / 曰&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My first thought was basically: &lt;em&gt;"Cool. I'll make a list of similar characters."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That lasted about five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A list of pairs wasn't enough
&lt;/h2&gt;

&lt;p&gt;The obvious data model was something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"characters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"未"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"末"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that doesn't tell me much.&lt;/p&gt;

&lt;p&gt;If I want to actually teach the difference, I need to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do learners really confuse these characters?&lt;/li&gt;
&lt;li&gt;Where did that evidence come from?&lt;/li&gt;
&lt;li&gt;What exactly makes them visually similar?&lt;/li&gt;
&lt;li&gt;What feature should the learner check first?&lt;/li&gt;
&lt;li&gt;Is this a beginner problem or an advanced one?&lt;/li&gt;
&lt;li&gt;Is it a pair, or part of a larger family?&lt;/li&gt;
&lt;li&gt;How confident am I that this group is actually useful?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suddenly my "list of similar characters" was turning into something closer to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SC-0032"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"characters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"未"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"末"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evidence_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"direct_learner_report"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"learner_level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"beginner"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"difference_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stroke_length"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"visual_rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Compare the two upper horizontal strokes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was the moment I realized this wasn't really a content list anymore. It was a small knowledge system.&lt;/p&gt;

&lt;h2&gt;
  
  
  I stopped asking "what looks similar?"
&lt;/h2&gt;

&lt;p&gt;At first, I could have generated hundreds or thousands of candidate pairs based on visual similarity.&lt;/p&gt;

&lt;p&gt;But there was an obvious problem: &lt;strong&gt;just because two characters look similar to an algorithm doesn't mean learners actually confuse them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I started collecting real evidence instead.&lt;/p&gt;

&lt;p&gt;Learner discussions were especially useful. Someone saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I know both of these, but I still get them wrong."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;is much more interesting than a similarity score.&lt;/p&gt;

&lt;p&gt;I also collected examples from teaching observations, research papers, and existing reference sets. Eventually the dataset grew to more than 200 canonical confusion groups.&lt;/p&gt;

&lt;p&gt;And that created another problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence needed its own structure
&lt;/h2&gt;

&lt;p&gt;Not every pair deserves the same confidence.&lt;/p&gt;

&lt;p&gt;There's a big difference between a learner explicitly reporting that they confuse 未 and 末, and a reference list saying two characters are graphically similar. Both are useful. But they shouldn't be treated as equivalent.&lt;/p&gt;

&lt;p&gt;So I ended up separating things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct learner evidence&lt;/strong&gt; — someone actually reports making the mistake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teaching / research evidence&lt;/strong&gt; — the confusion appears in classroom observations, experiments, or error data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference evidence&lt;/strong&gt; — the characters are documented as graphically similar, but I don't yet have strong evidence that learners frequently confuse them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction became surprisingly important later. It affects which pages I prioritize, which claims I can safely make, and which groups need more research before I treat them as real learning problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I discovered canonicalization
&lt;/h2&gt;

&lt;p&gt;Of course, real-world data immediately got messy.&lt;/p&gt;

&lt;p&gt;One source might give me &lt;code&gt;李 / 季&lt;/code&gt;. Another might give &lt;code&gt;季 / 李&lt;/code&gt;. Those aren't two problems — they're one confusion group with two sources.&lt;/p&gt;

&lt;p&gt;Larger families made this even more annoying: &lt;code&gt;撤 / 撒 / 散&lt;/code&gt; and &lt;code&gt;撒 / 散 / 撤&lt;/code&gt; — same group.&lt;/p&gt;

&lt;p&gt;So each confusion set eventually needed a canonical identity independent of character order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;李 / 季
季 / 李
    ↓
SC-0033
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds trivial. It isn't once you've already collected hundreds of records from different sources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; design your entity identity before you enthusiastically collect data. Ask me how I know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The visual difference became data too
&lt;/h2&gt;

&lt;p&gt;The next interesting problem was describing &lt;em&gt;why&lt;/em&gt; the characters are confusing.&lt;/p&gt;

&lt;p&gt;Once I had enough examples, patterns started appearing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stroke length&lt;/td&gt;
&lt;td&gt;未 / 末&lt;/td&gt;
&lt;td&gt;Same general structure, different relative horizontal lengths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aspect ratio&lt;/td&gt;
&lt;td&gt;日 / 曰&lt;/td&gt;
&lt;td&gt;Very similar strokes, different proportions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stroke position&lt;/td&gt;
&lt;td&gt;牛 / 午&lt;/td&gt;
&lt;td&gt;The important cue is where the strokes extend and intersect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component substitution&lt;/td&gt;
&lt;td&gt;李 / 季&lt;/td&gt;
&lt;td&gt;Shared lower structure, different top component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal position&lt;/td&gt;
&lt;td&gt;田 / 由 / 甲 / 申&lt;/td&gt;
&lt;td&gt;The central vertical changes position and extension&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So &lt;code&gt;difference_type&lt;/code&gt; became a real field rather than something buried in prose. I ended up with categories along the lines of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stroke_length
stroke_position
aspect_ratio
left_component
right_component
top_component
bottom_component
extra_stroke
shared_component
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then something interesting happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data model started designing the UI
&lt;/h2&gt;

&lt;p&gt;This was probably my favorite part.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If a confusion is caused by stroke length, the UI should highlight those strokes.&lt;/li&gt;
&lt;li&gt;If it's caused by a left component, highlighting the whole character just adds noise.&lt;/li&gt;
&lt;li&gt;If it's an aspect ratio problem, a bounding box might communicate the difference better than coloring individual strokes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That led to a product rule I really like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't highlight the character. Highlight the decision.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The data wasn't just generating content anymore. It was telling the interface what information deserved visual attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured data also made AI much more useful
&lt;/h2&gt;

&lt;p&gt;There was another benefit I didn't expect: generating learning content became much safer.&lt;/p&gt;

&lt;p&gt;The naive approach would be: &lt;em&gt;"Hey model, write a useful page explaining the difference between these Chinese characters."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That gives the model way too much freedom.&lt;/p&gt;

&lt;p&gt;Instead, I can provide known fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;characters&lt;/li&gt;
&lt;li&gt;pinyin&lt;/li&gt;
&lt;li&gt;meanings&lt;/li&gt;
&lt;li&gt;evidence&lt;/li&gt;
&lt;li&gt;difference type&lt;/li&gt;
&lt;li&gt;visual rule&lt;/li&gt;
&lt;li&gt;example vocabulary&lt;/li&gt;
&lt;li&gt;related confusion groups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then AI can help turn that structured information into readable explanations. That's a very different job.&lt;/p&gt;

&lt;p&gt;I don't want the model deciding whether learners confuse two characters. I don't want it inventing the visual difference. I want it helping explain evidence I've already structured.&lt;/p&gt;

&lt;p&gt;For this kind of product, I've found that distinction really useful:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Use AI to transform knowledge, not invent the knowledge model.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One feature turned into a learning pipeline
&lt;/h2&gt;

&lt;p&gt;What started as &lt;code&gt;未 / 末&lt;/code&gt; eventually became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Learner report
      ↓
Confusion group
      ↓
Canonical entity
      ↓
Evidence grading
      ↓
Visual mechanism
      ↓
Learning rule
      ↓
Side-by-side comparison
      ↓
Vocabulary and context
      ↓
Recognition practice
      ↓
SRS review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point, Similar Characters wasn't really an isolated feature anymore. It connected naturally to the rest of the learning system.&lt;/p&gt;

&lt;p&gt;A learner notices: &lt;em&gt;"I keep confusing these."&lt;/em&gt; Then the product can help them &lt;strong&gt;distinguish → understand → practice → review → remember&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That feels much more useful than simply giving them another dictionary entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;If I were starting again, I would not begin by trying to collect as many character pairs as possible.&lt;/p&gt;

&lt;p&gt;I'd start with maybe 20–30 high-confidence groups and design this first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Problem → Evidence → Canonical Entity → Difference Mechanism → Learning Intervention → Practice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then scale the dataset.&lt;/p&gt;

&lt;p&gt;Because changing your ontology after you've already enriched hundreds of records is... not my favorite way to spend an evening.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the project is now
&lt;/h2&gt;

&lt;p&gt;The dataset eventually became the &lt;a href="https://hanzihero.app/similar-characters" rel="noopener noreferrer"&gt;Similar Characters&lt;/a&gt; section of HanziHero. The project also connects these comparisons with HSK-based character and vocabulary learning and spaced repetition (SRS).&lt;/p&gt;

&lt;p&gt;But from a development perspective, the part I still find most interesting is how the whole thing started.&lt;/p&gt;

&lt;p&gt;A learner says: &lt;em&gt;"Why do I keep confusing these two characters?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It sounds like a content problem. Keep digging and suddenly you're designing entity IDs, evidence models, taxonomies, UI rules, and learning pipelines.&lt;/p&gt;

&lt;p&gt;Funny how often small product problems turn out that way.&lt;/p&gt;




&lt;p&gt;If you're building educational software, I'd be interested to hear if you've run into something similar — a problem that looked like "just content" until you realized there was a real data model hiding underneath it.&lt;/p&gt;

</description>
      <category>chinese</category>
      <category>nlp</category>
      <category>edtech</category>
      <category>datamodeling</category>
    </item>
  </channel>
</rss>
