Adding Kotlin DataFrame skill - #48
Conversation
Covers idiomatic kotlinx.dataframe usage across the three operating
contexts (Kotlin Notebook, Gradle with compiler plugin, plain Gradle):
schema generation, @DataSchema, common operations, IO, and gotchas
around the kotlin("plugin.dataframe") compiler plugin.
Introduces a new 'data' category in CATEGORIES and README for
kotlinx.* data manipulation libraries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original "schema-flows-in-chains-only rule" / "type is frozen at the declaration" framing implied a plugin-specific quirk. The reality is plain Kotlin immutability: every DataFrame operation returns a new instance, the plugin assigns each expression its own schema, and a `val` simply keeps the type of the instance it was bound to. Also merge the now-redundant pandas-immutability gotcha into the rewritten "operations on a `val` don't update that `val`" entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
devcrocod
left a comment
There was a problem hiding this comment.
Great work, I think a dataframe skill is a handy thing to have
Regarding this specific skill, I have a few comments:
The skill looks quite large and feels like it was written more for humans than for coding agents. Since skills are primarily used by code agents, it would be better to keep it shorter, more concise, and focused: without unnecessary explanations, duplicates, or redundant information. You may want to take a look at Anthropic’s recommendations here: https://code.claude.com/docs/en/best-practices
Also, in my opinion, this skill should be more general-purpose. For example, it currently contains a lot of information about the dataframe plugin. If a user doesn't use this plugin, the agent will still load a lot of irrelevant information, which may confuse it and waste tokens (money) unnecessarily
|
|
||
| `columnOf(1, 2, null)` infers `Int?`. Type inference looks at the actual runtime values. | ||
|
|
||
| ## Common operations (with-plugin idiomatic syntax) |
There was a problem hiding this comment.
maybe I can merge this with the cookbook?
There was a problem hiding this comment.
Is this file necessary?
Doesn't the AI already know the dataframe API?
If the API changes, this file may quickly become outdated
There was a problem hiding this comment.
that's true, but it could save it from searching deep through the sources, costing even more tokens. In my latest test with the skill, I didn't notice it unzipping a sources.jar once. It is true, outdated-ness is a problem
There was a problem hiding this comment.
That said, mistakes in the cookbook are indeed problematic. They weigh heavily in the context. I noticed in the tests that the agent would very strictly stick to the recipe as written and not deviate from it, even if the library itself has a better solution.
|
@devcrocod Thanks!
Which is funny because I used the generate-skill skill to create it :) I'll see what I can do to shorten it.
This was done on purpose because it's the most difficult thing to understand for an agent. DataFrame is the only library that "breaks" Kotlin in this way, and while it can figure out the API from the sources, it cannot figure out what the compiler plugin can do that way. |
|
First test: Following our simple 2025 KotlinConf DataFrame workshop. The task: https://github.com/Jolanrensen/KotlinConf2025Workshop/tree/2026-ai-profile solve part1a and part1b using mcp actions to run the notebook. Our solutions:These were removed before running the agents: part 1b: the kt file: https://github.com/Jolanrensen/KotlinConf2025Workshop/blob/unzipped/Part%201/src/main/kotlin/com/kotlinconf/dataWorkshop/part1csv/solutions/Part%201b%20Solutions.kt Without skill:https://github.com/Jolanrensen/KotlinConf2025Workshop/tree/2026-ai-profile-no-skills-solution Took quite a long time (30min +), spent a lot of time hitting OK on it trying to unzip source jars. The results were quite good, though.
df0
.rename { arter and ø and næblængde_mm and næbdybde_mm and luffelængde_mm and kropsmasse_g and køn and måledato }
.into(
"species",
"island",
"bill_length_mm",
"bill_depth_mm",
"flipper_length_mm",
"body_mass_g",
"sex",
"measurement_date",
)
With skill:https://github.com/Jolanrensen/KotlinConf2025Workshop/blob/2026-ai-profile-with-skill-solution A lot faster, about 10 minutes. Did not see it decompile jars anymore.
df0
.rename { arter }.into("species")
.rename { ø }.into("island")
.rename { næblængde_mm }.into("bill_length_mm")
.rename { næbdybde_mm }.into("bill_depth_mm")
.rename { luffelængde_mm }.into("flipper_length_mm")
.rename { kropsmasse_g }.into("body_mass_g")
.rename { køn }.into("sex")
.rename { måledato }.into("measurement_date")
ResultsUsing the skill was faster; it spent less time having to sift through source jars. However, it did seem to rely more on the functions written inside the skill. Since |
|
I'll check whether having the cookbook or not makes any difference using https://github.com/JetBrains/skills-ab-eval next week (when I have new tokens, oops XD) |
Yes, I understand that. However, my comment about the case where the user does not use the plugin still stands. In my view, a skill should either be very general, essentially an equivalent of best practices for working with dataframes. Or be focused on one specific task. I think it would be fine to add the skill as it is now, with a few parts polished, and then decide how to improve it based on actual usage |
|
I created and ran some tests with and without the skill in its current state using skills-ab-level and Air :) TL;DR, the skill helps a tiny bit in its current state, but not for each test. Sometimes it makes it worse, which is definitely not desirable. I'll try to optimize it slightly and rerun the tests, probably omitting the cookbook to see if there's any difference. kotlin-dataframe-skill-tasks.zip Results with the skill as is (summarized by Air): Eval Results —
|
| Task | Side | Reward | Steps | Tool calls | Cost |
|---|---|---|---|---|---|
| Salary | without-skill | 1.0 ✅ | 13 | 18 | $0.10 |
| Salary | with-skill | 1.0 ✅ | 17 | 22 | $0.15 |
| Titanic | without-skill | 1.0 ✅ | 17 | 21 | $0.16 |
| Titanic | with-skill | 1.0 ✅ | 14 | 19 | $0.14 |
| Netflix | without-skill | 1.0 ✅ | 14 | 19 | $0.12 |
| Netflix | with-skill | 1.0 ✅ | 11 | 16 | $0.09 |
Skill impact (with − without)
| Task | Δ reward | Δ steps | Δ cost | skill_helped |
|---|---|---|---|---|
| Salary | 0.0 | +4 | +$0.05 | ✗ |
| Titanic | 0.0 | −3 | −$0.02 | ✗ |
| Netflix | 0.0 | −3 | −$0.03 | ✗ |
skill_helped = Falsein all cases because reward is identical (both sides maxed out at 1.0). The efficiency gains on Titanic and Netflix are real but not captured by the binary reward signal.
Qualitative observations
| Aspect | without-skill | with-skill |
|---|---|---|
| First action | Inspects project files | Reads SKILL.md first |
| CSV reading | readCSV (deprecated alias) |
readCsv (correct modern API) |
| Titanic delimiter | Discovered via runtime failure | Knew to use delimiter = ';' upfront |
| Titanic locale | Did not handle European decimal commas | Used ParserOptions(locale = Locale("fr")) |
| Salary build failures | 0 | 1 (missing io.* import — fixed in one retry) |
| Titanic build failures | 2 | 1 |
… working with nested data
|
@devcrocod Made some small changes to the skill file (like adding imports, because I noticed agents struggling there), but I'm burning through my tokens really fast A/B testing... I'm not sure what's the best way forward. Merge as is and gather more data that way or do some more testing first. wdyt? |
Fixes #47
Adds a new skill to help working with the DataFrame library and its compiler plugin.