<?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: Koji M</title>
    <description>The latest articles on DEV Community by Koji M (@koji_m_f02cae8a899bf33627).</description>
    <link>https://dev.to/koji_m_f02cae8a899bf33627</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%2F4179452%2F8a3dd294-f4b9-4820-8f11-3d94694bea99.jpg</url>
      <title>DEV Community: Koji M</title>
      <link>https://dev.to/koji_m_f02cae8a899bf33627</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/koji_m_f02cae8a899bf33627"/>
    <language>en</language>
    <item>
      <title>jpomop: open code to convert Japanese DPC inpatient data to OMOP</title>
      <dc:creator>Koji M</dc:creator>
      <pubDate>Tue, 13 Oct 2026 07:03:41 +0000</pubDate>
      <link>https://dev.to/koji_m_f02cae8a899bf33627/jpomop-open-code-to-convert-japanese-dpc-inpatient-data-to-omop-1m23</link>
      <guid>https://dev.to/koji_m_f02cae8a899bf33627/jpomop-open-code-to-convert-japanese-dpc-inpatient-data-to-omop-1m23</guid>
      <description>&lt;p&gt;Japan has a standard format for some inpatient data called DPC. Many people in the OHDSI world use the OMOP Common Data Model. I could not find public code that connects the two, so I started writing it. The project is called jpomop, and the code is on GitHub: &lt;a href="https://github.com/kojikojikojikoji/jpomop" rel="noopener noreferrer"&gt;https://github.com/kojikojikojikoji/jpomop&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am a Japanese data scientist, and I live in London.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why DPC data matters
&lt;/h2&gt;

&lt;p&gt;DPC is used to classify acute inpatient stays and help set hospital payments in Japan. Hospitals in the system send standard files about each stay to the Ministry of Health, Labour and Welfare (MHLW). The ministry publishes the file layout every year in a long manual. The FY2026 edition has 244 pages (&lt;a href="https://www.mhlw.go.jp/content/12400000/001738680.pdf" rel="noopener noreferrer"&gt;MHLW, PDF&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The scale is large. The MHLW summary of the FY2024 discharged patient survey lists 1,767 DPC hospitals in the analysed data (&lt;a href="https://www.mhlw.go.jp/content/12404000/001682709.pdf" rel="noopener noreferrer"&gt;MHLW, PDF&lt;/a&gt;). Other hospitals also take part in the survey. Every hospital in the survey makes its files from the same manual for that year.&lt;/p&gt;

&lt;p&gt;jpomop reads two of those files. Form 1 (様式1) holds details about the patient and stay, with diagnoses coded in ICD-10. The EF file (入院EF統合ファイル) records billed services by day, including drugs and procedures, and ward information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OMOP
&lt;/h2&gt;

&lt;p&gt;The OMOP CDM is an open community standard for observational health data. The OHDSI CDM Working Group manages it (&lt;a href="https://ohdsi.github.io/CommonDataModel/" rel="noopener noreferrer"&gt;OHDSI&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In Europe, EHDEN says it has a network of over 100 data partners mapped to the OMOP CDM (&lt;a href="https://www.ehden.eu/" rel="noopener noreferrer"&gt;EHDEN&lt;/a&gt;). In England, the NHS Research Secure Data Environment Network agreed in 2023 to adopt the OMOP CDM (&lt;a href="https://www.hdruk.ac.uk/news/nhs-research-sde-network-agrees-to-adopt-common-data-model/" rel="noopener noreferrer"&gt;HDR UK&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;OMOP is used in many places where I would like Japanese data to be usable. Putting DPC data into OMOP tables is a first step toward using OHDSI tools and study code. Whether a study works also depends on the mappings and on what DPC records.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was missing
&lt;/h2&gt;

&lt;p&gt;I looked for open code that turns DPC files into OMOP tables. I did not find any. There is an open ETL for Japanese EHR tables from the Rinchu-net standard data tables (&lt;a href="https://github.com/rinchu-net/OMOP-ETL" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;). Its source list does not include DPC files.&lt;/p&gt;

&lt;p&gt;I wanted something anyone could run without real patient data. I wrote the converter from the public specification and tested it with synthetic data and the worked examples in the manual.&lt;/p&gt;

&lt;h2&gt;
  
  
  What jpomop does
&lt;/h2&gt;

&lt;p&gt;This is the quick start from the README:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
jpomop synth &lt;span class="nt"&gt;--n&lt;/span&gt; 100 &lt;span class="nt"&gt;--out&lt;/span&gt; synth/
jpomop validate &lt;span class="nt"&gt;--yoshiki1&lt;/span&gt; synth/yoshiki1.txt &lt;span class="nt"&gt;--ef&lt;/span&gt; synth/ef.txt
jpomop convert &lt;span class="nt"&gt;--yoshiki1&lt;/span&gt; synth/yoshiki1.txt &lt;span class="nt"&gt;--ef&lt;/span&gt; synth/ef.txt &lt;span class="nt"&gt;--out&lt;/span&gt; cdm.duckdb
jpomop report &lt;span class="nt"&gt;--cdm&lt;/span&gt; cdm.duckdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;synth&lt;/code&gt; makes fake data for 100 patients. &lt;code&gt;validate&lt;/code&gt; checks the files against the parts of the FY2026 specification that jpomop supports and lists the problems it finds. &lt;code&gt;convert&lt;/code&gt; writes the CDM tables to DuckDB. &lt;code&gt;report&lt;/code&gt; summarises the result.&lt;/p&gt;

&lt;p&gt;The validation step can be useful on its own. It does not need OMOP or a vocabulary. A hospital that does not use OMOP could still use it to check the supported fields in its DPC files. I have not tried this on real files yet.&lt;/p&gt;

&lt;p&gt;This table shows which OMOP tables jpomop writes from each input:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;DPC&lt;/th&gt;
&lt;th&gt;CDM table&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Form 1, whole-stay form&lt;/td&gt;
&lt;td&gt;person, visit_occurrence, observation_period&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EF, ward code for each day&lt;/td&gt;
&lt;td&gt;visit_detail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form 1, non-suspected diagnoses (ICD-10)&lt;/td&gt;
&lt;td&gt;condition_occurrence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EF, codes that start with 6&lt;/td&gt;
&lt;td&gt;drug_exposure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EF, codes that start with 1&lt;/td&gt;
&lt;td&gt;procedure_occurrence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form 1, discharge outcome 6 or 7&lt;/td&gt;
&lt;td&gt;death&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That makes eight CDM tables. Patient IDs are hashed.&lt;/p&gt;

&lt;p&gt;The manual tells hospitals to set the ward code for each service date, not the ward at discharge (page 193). jpomop makes one visit_detail row for each run of days on the same ward. The ward code is each hospital's own code.&lt;/p&gt;

&lt;p&gt;The specification lives in YAML files. Each field has a name, a length, a type and the page number in the manual. I did not copy the manual's text.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tricky parts
&lt;/h2&gt;

&lt;p&gt;Suspected diagnoses. Japanese claims data often carry diagnoses marked as "suspected". In Form 1, this is the modifier code 8002. jpomop leaves these out of condition_occurrence and counts them. I made this choice to avoid treating them as confirmed diagnoses.&lt;/p&gt;

&lt;p&gt;Dates. Form 1 has no date for each diagnosis, so jpomop uses the admission date as the condition start date. This matters for studies of when a condition began: admission may not be the start date. The EF file has a date for each service, so jpomop uses the recorded service dates for drugs and procedures.&lt;/p&gt;

&lt;p&gt;Form types. Form 1 has several form types for one stay. jpomop uses the form for the whole stay. It uses the day-90 interim form only when a stay has no whole-stay form yet. Ward-group forms and readmission forms are counted but not converted. In the manual, a ward-group form covers part of a stay (page 42). A readmission form joins two stays, and each of those stays also has its own form (page 125). So their days are already in the forms that jpomop uses. I have not checked this on real files.&lt;/p&gt;

&lt;p&gt;Diagnosis codes. DPC writes ICD-10 without a dot, and sometimes with a fifth character. jpomop tries the most specific Athena code first and then shorter ones. For example, S7200 becomes S72.00, then S72.0, then S72.&lt;/p&gt;

&lt;p&gt;Drug mapping. The EF file uses receipt codes for drugs. jpomop looks up each code in the public drug master to get the NHI drug price code. Then it looks up that code in the table by Kimura et al. (2025), which maps Japanese drugs to RxNorm concepts (&lt;a href="https://doi.org/10.17632/y3756v8237.1" rel="noopener noreferrer"&gt;dataset, CC BY 4.0&lt;/a&gt;; &lt;a href="https://doi.org/10.1016/j.dib.2025.111418" rel="noopener noreferrer"&gt;paper&lt;/a&gt;). The table has 12,178 rows for oral and injectable drugs. It does not cover every drug. In the October 2026 drug master, 11,956 of 19,222 items have a concept ID in the table. That is 62% of the items, not of real drug use. I have not checked these concept IDs against Athena yet. Drugs that patients bring from home are left out.&lt;/p&gt;

&lt;p&gt;Masters are not included. The code masters come from the Japanese body that reviews and pays health insurance claims. Its website terms prohibit using or reproducing its documents without permission. So the repository does not include them. Users download the masters, the Athena vocabularies and the Kimura table themselves. Then &lt;code&gt;jpomop vocab build&lt;/code&gt; joins them on the user's own machine. Without a vocabulary, diagnosis and drug concept IDs are 0, and the original codes stay in the source value columns.&lt;/p&gt;

&lt;p&gt;The repository has a PROVENANCE.md file. It lists every document used, with its version, URL, retrieval date and SHA-256 hash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status and honest limits
&lt;/h2&gt;

&lt;p&gt;jpomop is early. The current version number is 0.0.1. Here is what is true today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It reads and checks both files and writes eight CDM tables.&lt;/li&gt;
&lt;li&gt;44 tests pass. Some of them rebuild the worked examples in the FY2026 manual.&lt;/li&gt;
&lt;li&gt;I have tested it only with synthetic data and those manual examples.&lt;/li&gt;
&lt;li&gt;I have not run it on real hospital data.&lt;/li&gt;
&lt;li&gt;I have not measured mapping rates with a full Athena vocabulary.&lt;/li&gt;
&lt;li&gt;Procedures keep their DPC code as the source value. Their concept ID is 0 for now.&lt;/li&gt;
&lt;li&gt;It targets CDM v5.4. The OHDSI site now lists v5.5 as the current version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Synthetic data shows that the code works on the cases I made. It does not show that it works on real hospital files. Errors such as double counting or wrong joins between Form 1 and EF may not show up until the code is tested on real data.&lt;/p&gt;

&lt;p&gt;I plan to release v0.1 at the end of November 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you can help
&lt;/h2&gt;

&lt;p&gt;I am looking for two kinds of people.&lt;/p&gt;

&lt;p&gt;If you work with DPC data in Japan, I would like you to run jpomop inside your own environment. You do not need to share any data. Counts and error messages are enough. If my reading of the manual is wrong, I want to know.&lt;/p&gt;

&lt;p&gt;If you work with OHDSI tools, I would like your view on the mapping choices. How should suspected diagnoses be handled? Is the admission date a fair start date for conditions? What would make Japanese inpatient data useful in your network studies?&lt;/p&gt;

&lt;p&gt;Issues and pull requests are open on GitHub: &lt;a href="https://github.com/kojikojikojikoji/jpomop" rel="noopener noreferrer"&gt;https://github.com/kojikojikojikoji/jpomop&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code is under Apache-2.0. Mapping tables made by the project are under CC BY 4.0. This does not cover the masters, the Athena vocabularies or the Kimura table. They keep their own terms.&lt;/p&gt;

</description>
      <category>healthcare</category>
      <category>datascience</category>
      <category>opensource</category>
      <category>python</category>
    </item>
    <item>
      <title>partworth: hierarchical Bayes for choice-based conjoint in Python</title>
      <dc:creator>Koji M</dc:creator>
      <pubDate>Tue, 13 Oct 2026 07:02:35 +0000</pubDate>
      <link>https://dev.to/koji_m_f02cae8a899bf33627/partworth-hierarchical-bayes-for-choice-based-conjoint-in-python-3d9k</link>
      <guid>https://dev.to/koji_m_f02cae8a899bf33627/partworth-hierarchical-bayes-for-choice-based-conjoint-in-python-3d9k</guid>
      <description>&lt;p&gt;A choice-based conjoint (CBC) study shows people a few products and asks them to pick one. Then it does this again and again. Each product is a mix of features, such as brand, size and price. From the choices, you estimate how much each feature matters to each person. Then you answer business questions. Which product wins? What happens if we raise the price? How much more will people pay for a bigger size?&lt;/p&gt;

&lt;p&gt;A common method is hierarchical Bayes (HB). Many teams use Sawtooth CBC/HB for it. It is commercial and runs outside Python. So the work often looks like this: you estimate in one tool, export a file, and build a simulator in a spreadsheet. Each hand-off is a place for mistakes. The model's uncertainty is also often lost on the way, and the spreadsheet shows one share per product, with no range.&lt;/p&gt;

&lt;p&gt;I wanted to do the whole thing in Python. Python already has general choice models. For example, pymc-marketing has a MixedLogit model. As of October 2026, it takes a wide table and has no correlations between coefficients. It also has no Sawtooth-style files and no WTP. For CBC work, I needed a few specific things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a "none" option, for people who would buy nothing&lt;/li&gt;
&lt;li&gt;correlated tastes, because people who care about brand may also care about price&lt;/li&gt;
&lt;li&gt;a price coefficient that is negative for every respondent, so willingness to pay makes sense&lt;/li&gt;
&lt;li&gt;outputs in the format CBC teams already use, like a Sawtooth-style utilities file&lt;/li&gt;
&lt;li&gt;a market simulator that shows uncertainty&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I could not find one Python package that did all of this, so I wrote partworth. It is open source (Apache-2.0) and built on PyMC.&lt;/p&gt;

&lt;h2&gt;
  
  
  What partworth does
&lt;/h2&gt;

&lt;p&gt;The README splits the workflow into four steps.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your survey. You bring CBC answers in long format, one row per respondent, task and alternative. You can also import a wide design, for example from ChoiceDesign.&lt;/li&gt;
&lt;li&gt;Fit one model. ChoiceData checks the data. HierarchicalMNL runs HB estimation on PyMC. Then you get diagnostics: R-hat, effective sample size (ESS) and divergences.&lt;/li&gt;
&lt;li&gt;Read the results. You get part-worths for each respondent, attribute importances and RLH (a fit score for each person). You can write a Sawtooth-style CSV file.&lt;/li&gt;
&lt;li&gt;Answer business questions. You get market shares with credible intervals, price curves, and willingness to pay (WTP) for a feature.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 1 to 3 replace the "estimate, export, spreadsheet" routine. Step 4 uses the posterior draws, so every share, price curve and WTP comes with a credible interval.&lt;/p&gt;

&lt;p&gt;A few design choices are there because of how CBC data look. Each person usually answers only about 8 to 20 tasks, so the data for one person is thin. partworth uses a non-centred model, which usually samples better in this case. It puts an LKJ prior on the correlations between tastes. For price, you can add a sign constraint. Then the price coefficient is negative for every respondent, through a lognormal. Without this, a respondent whose price coefficient is near zero or positive gives a WTP that makes no sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick look
&lt;/h2&gt;

&lt;p&gt;partworth needs Python 3.12 or later. It is not on PyPI yet, so you install it from GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"partworth[nutpie,datasets] @ git+https://github.com/kojikojikojikoji/partworth"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS 26 or later, PyTensor 3.3.3 or earlier can fail with &lt;code&gt;ld: library 'd64' not found&lt;/code&gt;. Set &lt;code&gt;PYTENSOR_FLAGS=cxx=&lt;/code&gt; to avoid it.&lt;/p&gt;

&lt;p&gt;This is the Quick look example from the README. It uses simulated data, so you can paste it as it is.&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;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;partworth&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;partworth.datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;simulate_cbc&lt;/span&gt;

&lt;span class="c1"&gt;# 300 respondents, 12 tasks, 3 products + "none" per task.
# mu: true population means of the coded coefficients (price on the log scale).
&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;truth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;simulate_cbc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&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;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;A&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;B&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;C&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;size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;S&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;M&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;L&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;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.5&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&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="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                       &lt;span class="c1"&gt;# how often each level was shown and chosen
&lt;/span&gt;
&lt;span class="c1"&gt;# more tuning and draws than the defaults: the SD of "none" mixes slowly (see the guide)
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HierarchicalMNL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draws&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tune&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chains&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_seed&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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;diagnostics&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;         &lt;span class="c1"&gt;# R-hat, ESS, divergences, what to try
&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;part_worths&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                         &lt;span class="c1"&gt;# raw utilities, one column per level
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;part_worths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zc_diffs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;# zero-centred diffs
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;importances&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_sawtooth_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utilities.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;scenario&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;Ours&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;Theirs&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;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;A&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;B&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;size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;M&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;M&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;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
&lt;span class="n"&gt;sim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MarketSimulator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shares&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                  &lt;span class="c1"&gt;# shares with 90% credible intervals
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;price_curve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.5&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wtp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;L&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;simulate_cbc&lt;/code&gt; makes 300 fake respondents with known true values. The fit uses more tuning and more draws than the defaults. The SD of "none" mixes slowly in this example, and the user guide explains why. The last three lines answer the business questions. Each one prints an estimate together with its credible interval.&lt;/p&gt;

&lt;p&gt;For real data, &lt;code&gt;examples/camera.py&lt;/code&gt; runs the same steps on the public camera data from the R package bayesm. partworth downloads that data from CRAN on first use.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I checked it
&lt;/h2&gt;

&lt;p&gt;An HB package can give numbers that look fine and still be wrong. So the repository has validation scripts. Each script writes a report to &lt;code&gt;validation/results/&lt;/code&gt;. These are the main results.&lt;/p&gt;

&lt;p&gt;Comparison with R bayesm. I ran bayesm 3.1.7 (&lt;code&gt;rhierMnlRwMixture&lt;/code&gt;) and partworth on the camera data. Tasks 1 to 14 were for estimation. Tasks 15 and 16 were kept as hold-out tasks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For every coefficient, the respondent-level posterior means of the two packages correlate at 0.997 or more.&lt;/li&gt;
&lt;li&gt;The hold-out hit rate is 0.729 for partworth and 0.730 for bayesm.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two packages use different samplers and priors. bayesm uses random-walk Metropolis and an inverse-Wishart prior. partworth uses NUTS and an LKJ prior. Even so, the respondent-level posterior means move together very closely, and the two packages predict the hold-out tasks equally well.&lt;/p&gt;

&lt;p&gt;Parameter recovery. I simulated 300 respondents with 12 tasks each, using known true values. Then I checked if partworth could find them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Respondent-level 90% intervals covered 88.7% of the true values.&lt;/li&gt;
&lt;li&gt;5 of 6 population means and 6 of 6 SDs were inside their 90% intervals.&lt;/li&gt;
&lt;li&gt;For the coefficients, the convergence targets were met: R-hat max 1.006, bulk ESS min 1094, no divergences. The correlations are a different story (see below).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simulation-based calibration. I fitted 200 simulated data sets and looked at 8 population parameters. The smallest uniformity p-value was 0.017. None was below 0.006, the threshold after correcting for 8 tests. So the test found no problem. That is not a proof that the model is calibrated, but it is a useful check.&lt;/p&gt;

&lt;p&gt;Speed. On one laptop, 300 respondents took 74 seconds. 1,000 respondents took 290 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is not good enough yet
&lt;/h2&gt;

&lt;p&gt;Some targets were not met. I want to list them clearly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On the camera data, population means differ from bayesm by up to 0.21 of bayesm's posterior SD. My target was 0.1, so this target is not met. The two packages use different priors. When I made partworth's priors much wider, the difference grew to 0.92 SDs. The respondent-level means still correlated with bayesm at 0.998. So, at least on this data set, the population means are sensitive to the priors.&lt;/li&gt;
&lt;li&gt;In the recovery test, the price mean (on the log scale) was just outside its 90% interval. The true value was 0.30 and the interval was [0.13, 0.29].&lt;/li&gt;
&lt;li&gt;In the recovery test, correlations between tastes mixed slowly. They reached R-hat 1.013 and ESS 399. The targets are R-hat below 1.01 and ESS above 400. Before you report correlations, check their diagnostics. You may need more draws.&lt;/li&gt;
&lt;li&gt;In the calibration run, 15 of the 200 fits had a population ESS below 99. That makes the check weaker.&lt;/li&gt;
&lt;li&gt;The "Sawtooth-style" files follow Sawtooth's public manuals. They have not been compared with files made by Sawtooth software.&lt;/li&gt;
&lt;li&gt;The package is pre-alpha. The API may change. It is not on PyPI yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All examples use public or simulated data. The camera data comes from bayesm, which is GPL. partworth never includes that data and uses no bayesm code. The comparison runs R in a separate process.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is next
&lt;/h2&gt;

&lt;p&gt;The next step is a first release on PyPI. Some things are out of scope: experimental design (use ChoiceDesign for that), latent class, MaxDiff and ACBC.&lt;/p&gt;

&lt;p&gt;If you work with CBC data, please try it. Run the Quick look, then &lt;code&gt;examples/camera.py&lt;/code&gt;. If you have Sawtooth, I would like to see how its utilities file compares with partworth's. If a fit does not converge, or a number looks wrong to you, please open an issue. Simulated data that shows the problem is the most helpful.&lt;/p&gt;

&lt;p&gt;The code, the user guide and all validation reports are here: &lt;a href="https://github.com/kojikojikojikoji/partworth" rel="noopener noreferrer"&gt;https://github.com/kojikojikojikoji/partworth&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am Koji Miyazawa, a data scientist living in London. Thanks for reading.&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>statistics</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A language app that corrects the sentence you actually wrote</title>
      <dc:creator>Koji M</dc:creator>
      <pubDate>Mon, 12 Oct 2026 15:55:39 +0000</pubDate>
      <link>https://dev.to/koji_m_f02cae8a899bf33627/a-language-app-that-corrects-the-sentence-you-actually-wrote-3cb7</link>
      <guid>https://dev.to/koji_m_f02cae8a899bf33627/a-language-app-that-corrects-the-sentence-you-actually-wrote-3cb7</guid>
      <description>&lt;p&gt;Most language apps are good at getting words into your head. Fewer help with the next step: putting those words into a sentence of your own and finding out whether a native speaker would say it that way.&lt;/p&gt;

&lt;p&gt;GogaQ is an iPhone and iPad app that my company, Washitsu, makes for that step. You learn sentences grouped by situation, answer in your own words, and get a correction that shows what to delete, what to add and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a correction looks like
&lt;/h2&gt;

&lt;p&gt;Here is a Quick Composition round, with English as the native language and French as the language being learned.&lt;/p&gt;

&lt;p&gt;The prompt is &lt;em&gt;"Please send me the documents by tomorrow."&lt;/em&gt; Suppose you write &lt;em&gt;Envoie-moi les documents jusqu'à demain, s'il te plaît.&lt;/em&gt; It is close, and a French colleague would probably understand it.&lt;/p&gt;

&lt;p&gt;In this example, GogaQ scores it 78 out of 100 and marks the one word to change: &lt;em&gt;jusqu'à&lt;/em&gt; becomes &lt;em&gt;d'ici&lt;/em&gt;. Then it explains why, in English. &lt;em&gt;Jusqu'à demain&lt;/em&gt; means "until tomorrow", so the sentence sounds as if they should keep sending documents until then. A deadline ("by tomorrow") needs &lt;em&gt;d'ici demain&lt;/em&gt;. A second note covers politeness: &lt;em&gt;Envoie-moi&lt;/em&gt; (tu) is fine with a close colleague, while &lt;em&gt;Pourriez-vous m'envoyer&lt;/em&gt; (vous) suits a client or your manager. You can listen to the corrected sentence and a more natural alternative, and save it to your review cards.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feygj94kgefw4puk8q5f7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feygj94kgefw4puk8q5f7.png" alt="The AI correction screen: score, the corrected sentence, an alternative and an explanation in English" width="800" height="739"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The correction screen, split into two columns. The app screens in this post were rendered from the app with sample data.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The score measures how far your answer is from the corrected version, so a natural alternative can still lose a few points. What helps more than the number is seeing which word made the difference and why.&lt;/p&gt;

&lt;p&gt;The same kind of help runs through free conversation. When you reply to the AI in French, it suggests a more natural way to say what you meant, highlights useful expressions in its own reply, and lets you listen to or translate any line.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzpjvagn3i0jg5jdyirln.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzpjvagn3i0jg5jdyirln.png" alt="A free conversation in French, with a more natural rephrasing suggested under the learner's message" width="800" height="668"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Free talk. The shaded line under your message is the suggested rephrasing.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How a session goes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open the app.&lt;/strong&gt; Today's plan is waiting on the first screen (more on that below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learn sentences by situation.&lt;/strong&gt; The French sentence book has 2,705 sentences, sorted by situation and CEFR level. You can listen to every sentence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer in your own words&lt;/strong&gt;, then compare with the corrected sentence and the explanation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drill the grammar that tripped you up&lt;/strong&gt; by arranging words or filling in blanks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use it.&lt;/strong&gt; Free conversation with AI, short compositions with corrections, listening practice, and news articles written at your level with five comprehension questions at the end.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The wordbook is built from those sentences. The French wordbook has 3,611 words, each with its part of speech, meaning and level, and you can save any of them as flashcards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping track of your study
&lt;/h2&gt;

&lt;p&gt;The Today screen keeps you on schedule. It shows today's phrase, which days you studied this week, your progress toward your daily goal, and today's plan: a round of sentences, a Quick Composition and a syntax drill. Your streak and remaining points sit at the top. The Library shows a "Today's review" count for the cards that are due, so you know when you're caught up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvkotftmqfvnbnvif9udf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvkotftmqfvnbnvif9udf.png" alt="The Today screen and the Library, for someone learning French at B2" width="800" height="683"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Left: the Today screen with the weekly record and daily goal. Right: the Library with today's review.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Ten languages, eight native languages
&lt;/h2&gt;

&lt;p&gt;French is one of ten languages you can learn: English, Japanese, Chinese (Simplified), Korean, Spanish, French, German, Indonesian, Vietnamese and Thai. The app itself, including translations and grammar notes, is available in eight native languages: English, Japanese, Chinese, Korean, Vietnamese, Indonesian, German and Thai. Any combination works except learning your own language.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;English (CEFR): 2,552 sentences, 6,780 words&lt;/li&gt;
&lt;li&gt;Japanese (JLPT): 3,230 sentences, 5,702 words&lt;/li&gt;
&lt;li&gt;Chinese, Simplified (HSK): 1,991 sentences, 2,280 words&lt;/li&gt;
&lt;li&gt;Korean (TOPIK): 2,011 sentences, 2,448 words&lt;/li&gt;
&lt;li&gt;Spanish (CEFR): 2,093 sentences, 2,725 words&lt;/li&gt;
&lt;li&gt;French (CEFR): 2,705 sentences, 3,611 words&lt;/li&gt;
&lt;li&gt;German (CEFR): 2,100 sentences, 3,053 words&lt;/li&gt;
&lt;li&gt;Indonesian (CEFR): 2,111 sentences, 2,617 words&lt;/li&gt;
&lt;li&gt;Vietnamese (CEFR): 2,102 sentences, 2,596 words&lt;/li&gt;
&lt;li&gt;Thai (CEFR): 2,110 sentences, 2,607 words&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;English and Japanese come with pre-recorded AI voices, and English learners can choose British, American or Australian accents. The other languages are read aloud by your device's built-in text-to-speech.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's free
&lt;/h2&gt;

&lt;p&gt;The sentence books and wordbooks don't use points, and they are free with no time limit. The AI features (corrections, conversation, news) run on points. A new account gets 20 points to use within 30 days, which is roughly six AI conversations or ten composition corrections. After that you can subscribe or buy points as you need them. Purchased points don't expire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;GogaQ runs on iPhone (iOS 18 or later) and iPad (iPadOS 18 or later), and it's free to download:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App Store: &lt;a href="https://apps.apple.com/app/id6762157603" rel="noopener noreferrer"&gt;apps.apple.com/app/id6762157603&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Website (8 languages): &lt;a href="https://gogaq.washitsu-lab.com/" rel="noopener noreferrer"&gt;gogaq.washitsu-lab.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If there's a sentence you've never been sure how to say, in French or any of the other nine languages, that's a good one to try first.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>languagelearning</category>
      <category>ios</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Japanese has its own AI tells. I built an agent skill that fixes them.</title>
      <dc:creator>Koji M</dc:creator>
      <pubDate>Mon, 12 Oct 2026 13:31:16 +0000</pubDate>
      <link>https://dev.to/koji_m_f02cae8a899bf33627/japanese-has-its-own-ai-tells-i-built-an-agent-skill-that-fixes-them-385d</link>
      <guid>https://dev.to/koji_m_f02cae8a899bf33627/japanese-has-its-own-ai-tells-i-built-an-agent-skill-that-fixes-them-385d</guid>
      <description>&lt;p&gt;Most of the Japanese I write for work starts as an AI draft: proposals for small businesses, progress reports, Slack updates to people outside engineering. The facts are usually right. I still can't send them as they are, and I kept fixing the same things by hand.&lt;/p&gt;

&lt;p&gt;So I turned those fixes into an agent skill and published it. It works with Claude Code, Codex and Cursor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kojikojikojikoji/natural-japanese-writing" rel="noopener noreferrer"&gt;https://github.com/kojikojikojikoji/natural-japanese-writing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You don't need to read Japanese to use it. You hand the agent a Japanese text and ask it to sound less AI-written.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I kept running into
&lt;/h2&gt;

&lt;p&gt;For a long time I couldn't explain what was wrong. The grammar was fine. A few things that actually happened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A slide deck for a client still had 御社 ("your company") in six places. It's the spoken form. In writing you use 貴社, and a client notices.&lt;/li&gt;
&lt;li&gt;A proposal that executives would sign off on said 「稼働中の版には触れません」, roughly "we won't touch the running version". Engineers get it. The people approving the budget had no idea what it meant for them.&lt;/li&gt;
&lt;li&gt;A Slack update to a business team listed file names and line numbers. Nobody on that team opens the repository.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add the bold "&lt;strong&gt;Label:&lt;/strong&gt;" bullets, the "it's not X, it's Y" lines and paragraphs ending in 「重要です」 ("this is important"). Every time someone pointed one out, I added a line to my CLAUDE.md. The file kept growing and the agent followed it some days and not others.&lt;/p&gt;

&lt;p&gt;I tried the English skills first, &lt;a href="https://github.com/blader/humanizer" rel="noopener noreferrer"&gt;humanizer&lt;/a&gt; and &lt;a href="https://github.com/conorbronsdon/avoid-ai-writing" rel="noopener noreferrer"&gt;avoid-ai-writing&lt;/a&gt;. They look for "delve", "tapestry" and em dashes. On a Japanese report they found almost nothing. So I started collecting Japanese tells from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I approached it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Decide who's reading before fixing anything.&lt;/strong&gt; The skill first labels the text as a business document, casual reading, or a manual. The usual trick for sounding human is to add a personal aside. In a blog post that works. In a proposal to a Japanese client it reads as careless. Without this step, you fix one problem by creating another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Split the tells into two kinds.&lt;/strong&gt; Japanese research and measurement posts pointed the same way. Some tells are templates: bold labels, "not X but Y", opening with 「本記事では〜について解説します」 ("In this article, I will explain…") and closing with 「いかがでしたか」 ("How was it?"). Those can be found and deleted. The other kind is evenness: sentence length, sentence endings, paragraph length and comma spacing are all about the same. Human Japanese is lumpier. You can't fix that one phrase at a time, so the skill rewrites the whole paragraph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't convict on one tell.&lt;/strong&gt; People pick up AI phrasing too, and some "AI phrases" are just normal Japanese. There are 59 tells across six layers. The 25 clearest ones are starred and fixed on sight. The rest are fixed only when three or more different ones land in the same paragraph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Count what can be counted.&lt;/strong&gt; An agent reading a checklist misses tells in its own output. So bold text, em dashes, "not X but Y", stock words, repeated endings and sentence-length variance are counted by &lt;code&gt;scripts/check.py&lt;/code&gt; (plain Python, no dependencies). After rewriting, the skill runs it again and only returns the text once the starred tells it can count are gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does to a real draft
&lt;/h2&gt;

&lt;p&gt;I asked a model for a Slack message telling my manager a project is two weeks behind. It returned this (English gloss in brackets):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;お疲れ様です。○○プロジェクトについて、進捗が当初のスケジュールから &lt;strong&gt;2 週間遅延&lt;/strong&gt; しています。主な要因は以下の通りです。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;要件定義の変更&lt;/strong&gt; が多数発生し、再設計に時間を要したこと。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;外部ベンダーの納品遅延&lt;/strong&gt; に伴う依存タスクの遅れ。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;リソース不足&lt;/strong&gt;（開発メンバーの一時的な兼任業務）&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;現在、遅延分の取り戻しに向けて、次の対策を実施中です。&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;/ul&gt;

&lt;p&gt;つきましては、&lt;strong&gt;新たなマイルストーン案&lt;/strong&gt;（○月○日 完了予定）をご提示いたしますので、ご確認いただけますと幸いです。ご質問やご要望がございましたら、遠慮なくお知らせください。&lt;/p&gt;

&lt;p&gt;[The project is &lt;strong&gt;2 weeks delayed&lt;/strong&gt;. Main factors: 1. &lt;strong&gt;Requirement changes&lt;/strong&gt; … 2. &lt;strong&gt;Vendor delivery delay&lt;/strong&gt; … 3. &lt;strong&gt;Resource shortage&lt;/strong&gt; … Countermeasures: reallocate tasks, re-negotiate with the vendor, strengthen early detection. We hereby present a &lt;strong&gt;new milestone proposal&lt;/strong&gt; and would be grateful for your review. Please do not hesitate to contact us with any questions.]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Five bold phrases in a Slack message to your own manager, bullet points written like slide notes, and a sign-off copied from a customer-service template. After the skill:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;お疲れ様です。○○プロジェクトが当初の予定から2週間遅れています。&lt;/p&gt;

&lt;p&gt;要件の変更が続いて再設計に時間がかかったことと、外部ベンダーの納品が遅れ、それを待つ作業も遅れたことが主な原因です。開発メンバーの一部は別の業務を兼任していて、人手も足りていませんでした。&lt;/p&gt;

&lt;p&gt;今は日程に余裕のある作業から人を外して遅れている作業に回し、ベンダーとは納期を調整し直しています。今後の遅れに早く気づけるよう、毎週の定例会議で進捗を細かく確認するようにしました。&lt;/p&gt;

&lt;p&gt;完了予定を○月○日に見直す案をお送りしますので、ご確認をお願いします。&lt;/p&gt;

&lt;p&gt;[The project is two weeks behind. Mostly because requirements kept changing and the redesign took time, and because a vendor delivered late, which held up the work that depended on it. Some of the team were also covering other jobs, so we were short-handed. We've moved people off tasks with slack in the schedule onto the late work, and re-agreed the dates with the vendor. To catch future slips earlier, we now go through progress in more detail at the weekly meeting. I'll send a proposal to move completion to [date]. Could you take a look?]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Same delay, same causes, same countermeasures, same request for a new date. Nothing was added. &lt;code&gt;check.py&lt;/code&gt; counted six starred tells before and none after.&lt;/p&gt;

&lt;p&gt;How you ask changes the fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Rewrite this for the business team": file names and table names become "who is affected and how".&lt;/li&gt;
&lt;li&gt;"Make this read like a blog post": the writer's doubts and asides stay in.&lt;/li&gt;
&lt;li&gt;Load it before writing: the agent writes to the standard from the start. In my own use that's quicker than fixing afterwards.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Did the tells hold up?
&lt;/h2&gt;

&lt;p&gt;I compared 40 human texts (Slack replies from 14 people) with 40 AI texts (15 everyday prompts such as reports, emails and articles; 15 texts each from Gemini 3.1 Pro and GPT-OSS 120B, 10 from Claude Sonnet 4.6). For each metric I computed the AUC, the chance that a random AI text scores higher than a random human one. 0.5 is a coin flip. Sentence-length variance is lower in AI text, so that row is flipped to read the same way.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;AUC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Starred tells, total&lt;/td&gt;
&lt;td&gt;0.91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bold text&lt;/td&gt;
&lt;td&gt;0.89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentence-length variance (lower in AI)&lt;/td&gt;
&lt;td&gt;0.69&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commas per sentence&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Em dashes&lt;/td&gt;
&lt;td&gt;0.51&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Some guesses were wrong. The em dash (—), the best-known English tell, barely showed up in Japanese from these three models. And two phrases usually filed under machine-like translationese, 「に関して」 ("regarding") and 「〜することができます」 ("is able to"), were used more by the humans, so I took them off the starred list. The human side is all Slack, which doesn't match the AI prompts and where people rarely use bold, so that number is probably inflated. Details are in &lt;code&gt;eval/README.md&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.claude/skills
git clone https://github.com/kojikojikojikoji/natural-japanese-writing ~/.claude/skills/natural-japanese-writing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cursor&lt;/strong&gt; reads &lt;code&gt;~/.claude/skills/&lt;/code&gt; too, so that's all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codex&lt;/strong&gt; reads &lt;code&gt;~/.agents/skills/&lt;/code&gt;: &lt;code&gt;mkdir -p ~/.agents/skills &amp;amp;&amp;amp; ln -s ~/.claude/skills/natural-japanese-writing ~/.agents/skills/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then ask: &lt;em&gt;"Make this Japanese sound less AI-written."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It's MIT-licensed. If you find a tell I missed, or Japanese text that slipped past it, please open an issue. I add a line every time someone points one out to me.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>japanese</category>
      <category>claude</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
