AI Dev Lab
ChartPress

ChartPress ができるまで — CSV を投げるだけの統計ダッシュボード

CSV ダッシュボードの開発ログ。 引用符と BOM に対応した自前の CSV パーサ、 8 割ルールで列の型を決める推定、 要約統計・ヒストグラム・Pearson 相関ヒートマップの実装を記録しています。

ChartPress は、 CSV / TSV を 投げ込む だけ で 列 ごと の 型 ・ 要約統計 ・ ヒストグラム ・ 相関 ヒートマップ を 一気 に 出す ダッシュボード。 パーサ も 統計 も 描画 も 素 の JavaScript で 書いて あり、 データ は ブラウザ の 外 に 出ません。

#🎯 「Excel を 開く 前 の 5 分」 を 埋める

営業会議 で 渡さ れる CSV、 役所 の オープンデータ、 アンケート の 集計結果。 これら を 受け取った とき に 最初 に やる の は 分析 では なく、 「何 列 あって、 どれ が 数値 で、 欠損 が どれ くらい か」 を 掴む こと です。

表計算 ソフト で やる と 列 を 一つ ずつ 見る 羽目 に なる し、 Python を 出す ほど でも ない。 その 隙間 を 埋める の が 狙い でした。 だから 操作 は 「投げる」 の 一手 だけ に して、 設定 画面 を 作ら ない と 決め ました。

社外 の データ を 扱う 以上、 アップ ロード 先 が ある 時点 で 使え ない 場面 が あり ます。 端末 内 で 完結 する の は 演出 では なく 前提 条件 でした。

#🏗️ CSV パーサ を 自前 で 書く

ライブラリ を 入れ ず に 書き ました。 CSV は 一見 「カンマ で split」 です が、 それ だ と 引用符 の 中 の カンマ で 壊れ ます。

ts
if (inQuotes) {
  if (ch === '"') {
    if (src[i + 1] === '"') { cell += '"'; i += 2; continue }  // "" は エスケープ
    inQuotes = false; i++; continue
  }
  cell += ch; i++; continue
}

状態 は inQuotes 一つ だけ。 RFC 4180 の うち 実務 で 効く ところ (引用符 ・ "" エスケープ ・ CRLF ・ BOM 除去 ・ タブ 区切り) に 絞る と、 この 大きさ で 収まり ます。 BOM 付き の Excel 出力 が そのまま 通る か どうか は、 日本 の CSV では 効き ます。

#🔑 列 の 型 を 8 割 で 決める

一番 悩んだ の が 型推定 です。 「1 個 でも 数値 で ない 値 が あれば テキスト」 に すると、 現実 の CSV は ほぼ 全部 テキスト に なり ます。 欠損 や 「N/A」 や 全角 が 必ず 混ざる から です。

そこで 8 割 ルール に しました。

ts
if (numCount / ratio >= 0.8) return "numeric"
if (dateCount / ratio >= 0.8) return "date"
const unique = new Set(nonEmpty).size
if (unique <= 20 && unique / ratio <= 0.5) return "categorical"
return "text"

数値 判定 は カンマ 区切り (1,234)、 末尾 の %、 指数表記 を 落とし込んで から Number() に 通し ます。 日付 は 2024/03/012024年3月1日 を ISO に 直して から Date へ。

カテゴリ 判定 の 条件 が 二段 に なって いる の が ポイント で、 「種類 が 20 以下」 かつ 「行数 の 半分 以下 の 種類 数」。 前者 だけ だ と 20 行 の データ で 全部 が カテゴリ 扱い に なり、 後者 だけ だ と 1 万 行 の 自由記述 が カテゴリ に なり ます。

#📊 統計 と 相関

数値列 に は 件数 / 欠損 / 平均 / 中央値 / Q1 / Q3 / 最小 / 最大 / 標準偏差 / 合計 を 出し ます。 四分位 は ソート 済み 配列 から の 線形補間。

ヒストグラム は 最小 と 最大 の 間 を 等分 する 単純 な 方式 で、 端 の 値 が こぼれ ない よう に 最終 ビン へ 寄せて います。

ts
let idx = Math.floor((v - min) / step)
if (idx >= binCount) idx = binCount - 1

max そのもの が idx === binCount に なる 境界 で、 これ を 入れ 忘れる と 最大値 だけ が どの ビン に も 入ら なく なり ます。

相関 は 数値列 の 総当たり で Pearson を 計算 して ヒートマップ に。 列 が 増える と 組み合わせ は 二乗 で 増え ます が、 100 行 数十列 の 即席 分析 が 想定 な ので、 総当たり の まま で 十分 でした。

#😓 苦労した ところ

相関 を 出す と 因果 に 見えて しまう。 ヒートマップ は 分かり やすい ぶん、 赤い マス を 見た 人 が 「A が B の 原因」 と 読み がち です。 数字 と 一緒 に 「相関 で あって 因果 では ない」 を 画面 に 置き、 色 も 過度 に 煽ら ない 段階 に 抑え ました。

サンプル データ を 何 に する か。 空 の 画面 に 「CSV を 入れて ください」 と だけ 出て いる と、 何 が 起きる の か 分から ず に 離脱 され ます。 店舗別売上 ・ 東京 の 気温 ・ クラス の 成績 ・ マラソン の 心拍 ・ 23 区 の 家賃 ・ ツール 利用 調査 の 6 種 を 同梱 し、 1 クリック で 全部 の 機能 が 動く 状態 から 始まる ように しました。 型推定 が 4 種類 とも 出る よう、 数値 ・ 日付 ・ カテゴリ ・ テキスト が 混ざる 構成 に して あり ます。

大きい ファイル。 数万 行 を 投げる と 型推定 の 全走査 で 一瞬 固まり ます。 現状 は 想定 規模 を 明記 する ところ で 止めて いて、 分割 処理 は 入れて いません。

#🔭 今後 の 拡張

  • 大きい ファイル の 逐次 パース (Web Worker + ストリーム)
  • 散布図 と 箱ひげ図 (相関 の 中身 を 見る に は ヒートマップ だけ で は 足り ない)
  • 列 の 型 を 手動 で 上書き する 指定
  • 結果 の PNG / PDF 書き出し

#💡 この サービス から 言える こと

「自動 で 判定 する」 機能 は、 閾値 を どこ に 置く か が ほぼ すべて でした。 8 割 と いう 数字 は 理論 から 出た もの では なく、 実際 の CSV を 何本 か 通して 決め た もの です。 こう いう 数字 は 後 から 触れる よう コード の 一箇所 に 置いて おく の が 正解 でした。

もう 一つ は、 入力 を 求める ツール は 空 の 状態 が 一番 難しい と いう こと。 サンプル を 同梱 する か どう か で、 同じ 機能 でも 伝わり 方 が まったく 変わり ます。

[ ./next_action ]

読んだら、 ChartPress を実際に動かす。

この開発ログは ChartPress をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ToonCast

ToonCast ができるまで — AnimeGANv2 をブラウザで動かす

AnimeGANv2 の小さな ONNX (約9MB) を onnxruntime-web (単一スレッド WASM=COOP/COEP不要、 color-revive で承認済みライブラリの再利用) で実行。 512x512・[-1,1] 正規化で推論し、 結果を元解像度に戻して表示する設計記録。 写真は端末内処理。

read log →
ColorRevive

ColorRevive ができるまで — onnxruntime-web で白黒写真をカラー化

DeOldify の量子化 ONNX を onnxruntime-web (CDN side-load・単一スレッド WASM=COOP/COEP不要) で実行。 256x256 でモデル推論し、 輝度は元写真・色だけ AI を YCbCr で再合成して輪郭を保つ設計記録。 写真は端末内処理。

read log →
PhotoTwin

PhotoTwin ができるまで — CLIP画像埋め込みで似た写真を見つける

CLIP (Xenova/clip-vit-base-patch32) の image-feature-extraction を transformers.js の CDN ESM で side-load し、 各写真を正規化ベクトル化。 cosine 類似度で重複・似た写真をブラウザ内で検出する設計記録 (新ライブラリ追加なし=what-cam と同じ CLIP の再利用)。

read log →
AkinFind

AkinFind ができるまで — ブラウザ内で完結する embeddings 意味検索

多言語の文章埋め込みモデル (Xenova/multilingual-e5-small) を transformers.js の CDN ESM で side-load し、 各文を正規化ベクトル化。 cosine 類似度で意味検索と似ている文ペア検出を全て端末内で行う設計記録。

read log →
WhatCam

WhatCam ができるまで — CLIP のゼロショット画像分類をブラウザで動かす

CLIP (Xenova/clip-vit-base-patch32) を transformers.js の CDN ESM で side-load し、 写真と候補ラベルの近さをブラウザ内で計算。 日本語ラベルを英語プロンプトに変換し、 図鑑と自由入力の両モードで「これ何?」を判定する設計記録。

read log →
DepthCast

DepthCast ができるまで — 1枚の写真をAIの深度推定で立体にする

Depth Anything (transformers.js) を CDN ESM で side-load し、 1枚の写真から深度マップを推定。 WebGL2 フラグメントシェーダで深度に比例した視差 (iterative backward parallax) を作り、 赤青アナグリフ / WebM 書き出しまで端末内で完結させた設計記録。

read log →