難易度を上げたコード課題で比較したら、1モデルだけ「行」と「値」を読み違えていた

きっかけ:単語数え上げ課題は簡単すぎた

これまでのコーディング比較で使ってきた「テキストファイルの頻出単語トップ5」課題、正直もう簡単すぎて差がつきにくくなってきた。今回はもう少し歯ごたえのある課題——CSVの集計・欠損値処理・重複データの統合・ソートを組み合わせたタスクで、Gemma4-12B-Heretic・gpt-oss-safeguard-20B・Qwen3-30B-A3B-2507・Qwen2.5-14Bの4モデルを比較した。

課題内容

生徒名と3科目の点数が入ったCSVを読み込み、平均点から成績(A〜F)を判定するプログラムを書かせた。仕込んだ条件はこう。

  • 点数が欠損している、または数値に変換できない行はスキップ
  • 同じ名前が複数回出てきたら、平均点が高い方を採用
  • 平均点の高い順にソートして出力

テストデータには、わざと欠損値(Carol: 1科目分の点数が空欄)・不正値(Frank: 点数の1つが"abc")・重複名(Aliceが2回登場、点数が違う)を混ぜておいた。正解は一意に決まる——Frank・Carolは行ごと除外、Aliceは高い方の平均点(91.7点)を採用、という結果になるはずだった。

結果:3モデルは完璧、1モデルだけ仕様を読み違えた

仕様遵守チェックのマトリクス

Gemma4-12B-Heretic・gpt-oss-safeguard-20B・Qwen3-30B-A3B-2507の3モデルは、実際に実行した結果が完全に一致した。

Eve: 97.7(A)
Alice: 91.7(A)
Bob: 70.0(C)
Dave: 55.0(F)

Carol・Frankは正しく除外され、Aliceは高い方の平均点(91.7点)がきちんと採用されていた。重複データの統合・欠損値の行スキップ・不正値の検出まで、複雑な条件を全て満たした完璧な実装だった。

ところがQwen2.5-14Bだけ、この基準から外れた。

Eve: 97.7(A)
Alice: 91.7(A)
Frank: 90.0(A)
Carol: 82.5(B)
Bob: 70.0(C)
Dave: 55.0(F)

FrankとCarolが消えずに残ってしまっている。コードを確認すると、原因がはっきり分かった。

def calculate_average(scores):
    valid_scores = [float(score) for score in scores if score.isdigit()]
    if not valid_scores:
        return None
    return sum(valid_scores) / len(valid_scores)

指示は「点数が欠損・不正なはスキップ」だったのに、Qwen2.5-14Bは不正な値だけを取り除いて、残った点数だけで平均を計算していた。Frankは「abc, 90, 90」のうち"abc"だけ除外して(90+90)/2=90.0点、Carolは欠損分を除いて(80+85)/2=82.5点、という具合に、行単位ではなくフィールド単位でスキップする実装になっていた。

一見「柔軟に対応してくれた」ようにも見えるけど、これは明確に指示と違う仕様。しかも構文エラーにもならず、静かに間違った結果を出す(それらしい数値が出てくるので目視だけだと気づきにくい)という点で、実務では厄介なタイプの不具合だった。

結論

  1. 複雑な条件が絡む課題でも、多くのモデルは正確に実装できる — 今回は4モデル中3モデルが、欠損値・重複データ・ソートを含む複雑な仕様を完璧に満たした
  2. Qwen2.5-14Bは「行スキップ」を「フィールドスキップ」に読み替えてしまった — 指示の解釈を誤ったことで、本来除外すべきデータが結果に混入した
  3. この手のミスは実行しないと気づけない — コードの見た目は自然で、実行してもエラーにならず、出力もそれらしい数値が並ぶため、実際に既知の正解と突き合わせないと発覚しない
  4. 単純な課題での比較には限界がある — 今回のようにエッジケースを複数仕込んだ課題にすることで、初めて見えてくる実装の粗さがあった

コーディング課題の難易度を上げたことで、これまでの単純な課題では見えなかった「仕様解釈の精度」という新しい評価軸が見つかった。次にモデル比較をする時は、こういう複合的な条件を含む課題を標準にしていきたい。