.NET MAUI BlazorでCSV株価を読み込みML.NET(Ssa)予測とChart.jsグラフ表示ができない原因と解決策

.NET MAUI(Blazor)アプリで「CSV株価を読み込む → ML.NET(Ssa)でHighを予測 → 表とチャートで可視化」を作ったのに、データが入らず・予測で例外が出て・グラフも表示されない。こうした“全部が止まる”ケースは、原因が1つではなく「CSVパースの失敗」と「ML.NETが扱えない型/パラメータ設定」が連鎖していることが多いです。この記事では、再現しやすい落とし穴の切り分け手順と、MAUI Blazor+Chart.jsで確実に描画まで到達する実装例をまとめます。

目次

今回の症状が起きる全体像

処理の流れはシンプルに見えますが、どこか1点で失敗すると“後段がすべて空振り”になりやすい構造です。

症状よくある根本原因確認ポイント最短の対処
CSVをアップロードしても表が空日付や数値のパース失敗で1行も追加されていない読み込み件数が0になっていないか日付フォーマット修正+Trim+失敗行ログ
ForecastHighValues()で例外ML.NETが推論できない型(decimal等)/学習パラメータがデータ数に不釣り合い入力クラスの型・件数・windowSize/trainSizefloat/doubleへ統一+パラメータ自動調整
チャートが表示されないデータが空/JSInteropの呼び出しタイミング/JS関数が読み込まれていないlabels/valuesの長さ、ブラウザコンソールエラーOnAfterRenderで描画+既存チャート破棄+null埋め

ポイントは「チャートが出ない」を“チャートの問題”だと決めつけないことです。多くの場合、上流のCSV取り込みが0件になっていて、描画するデータが存在しないだけです。

CSV読み込みでつまずく典型パターン

質問のCSV例は以下のような形式でした。

Date ,Open ,High ,Low ,Close ,Shares Traded ,Turnover
28-Feb-24,37926.65,38138.35,37594.5,37741.75,14799770,2511.43
...

ここで落とし穴になりやすいのが次の3点です。

  • ヘッダー行の扱い(スキップはできていても、後から読みにくくなりがち)
  • 日付フォーマットの不一致(2桁年を4桁でパースしようとして全滅)
  • Trim不足(列名や値の前後スペース、行末の\rが混ざってパース失敗)

ヘッダー行スキップは「意図が伝わる形」に整える

ヘッダーを飛ばすだけなら foreach (var line in lines.Skip(1)) でも動きますが、保守性を考えると「ヘッダーを除外した行だけ」を先に作っておく方が安全です。デバッグ時に“いま何行を回しているのか”も追いやすくなります。

private List<StockData> ParseCsv(string content)
{
    var lines = content
        .Split('\n', StringSplitOptions.RemoveEmptyEntries)
        .Skip(1)
        .ToList();

    var data = new List<StockData>();

    foreach (var line in lines)
    {
        // ここで列をパース
    }

    return data;
}

ただし実運用では、Split('\n')よりもStringReaderで1行ずつ読む方が堅牢です(\r\n混在、巨大ファイル、空行などに強い)。次のセクションで、より安全な実装例を提示します。

日付フォーマットは「dd-MMM-yy」が正解

CSVの日付が 28-Feb-24 のように年が2桁なのに、コード側が dd-MMM-yyyy(4桁年)だと、TryParseExactが常に失敗します。結果、1件も追加されず、表もチャートも空になります。

NG例(4桁年でパース)

bool ok = DateTime.TryParseExact(
    columns[0],
    "dd-MMM-yyyy",
    CultureInfo.InvariantCulture,
    DateTimeStyles.None,
    out var date);

OK例(2桁年でパース)

bool ok = DateTime.TryParseExact(
    columns[0],
    "dd-MMM-yy",
    CultureInfo.InvariantCulture,
    DateTimeStyles.None,
    out var date);

ここが直るだけで「CSVのデータが表示されない」系の問題は一気に解消します。逆に言うと、チャートが出ないときは最初にこの部分を疑うのが最短ルートです。

Trimと\r対策まで含めた“実戦向け”CSVパース例

CSVヘッダーには Date ,Open ,High ... のように列名の後ろにスペースが入っています。またWindowsの行末は\r\nなので、Split('\n')だと末尾に\rが残るケースがあります。これらは数値パース失敗の原因になります。

次の実装は以下をまとめて対策します。

  • StringReaderで1行ずつ読む
  • 列を StringSplitOptions.TrimEntries でTrim
  • 日付は dd-MMM-yy 固定(InvariantCulture)
  • 数値は NumberStyles.Float | AllowThousands
  • 失敗した行はスキップしつつ、どこで落ちたか追えるようにする
using System.Globalization;

public class StockData
{
    public DateTime Date { get; set; }
    public float Open { get; set; }
    public float High { get; set; }
    public float Low { get; set; }
    public float Close { get; set; }
    public float SharesTraded { get; set; }
    public float Turnover { get; set; }
}

private List<StockData> ParseCsvRobust(string content)
{
    var data = new List<StockData>();

    using var reader = new StringReader(content);
    string? line;
    bool headerSkipped = false;

    var numberStyle = NumberStyles.Float | NumberStyles.AllowThousands;
    var culture = CultureInfo.InvariantCulture;

    while ((line = reader.ReadLine()) != null)
    {
        if (!headerSkipped)
        {
            headerSkipped = true;
            continue;
        }

        if (string.IsNullOrWhiteSpace(line))
            continue;

        var columns = line.Split(',', StringSplitOptions.TrimEntries);

        if (columns.Length < 7)
            continue;

        // BOM対策(先頭列に混入することがある)
        var dateText = columns[0].Trim('\uFEFF');

        if (!DateTime.TryParseExact(dateText, "dd-MMM-yy", culture, DateTimeStyles.None, out var date))
            continue;

        if (!float.TryParse(columns[1], numberStyle, culture, out var open)) continue;
        if (!float.TryParse(columns[2], numberStyle, culture, out var high)) continue;
        if (!float.TryParse(columns[3], numberStyle, culture, out var low)) continue;
        if (!float.TryParse(columns[4], numberStyle, culture, out var close)) continue;
        if (!float.TryParse(columns[5], numberStyle, culture, out var shares)) continue;
        if (!float.TryParse(columns[6], numberStyle, culture, out var turnover)) continue;

        data.Add(new StockData
        {
            Date = date,
            Open = open,
            High = high,
            Low = low,
            Close = close,
            SharesTraded = shares,
            Turnover = turnover
        });
    }

    // 時系列モデル用に昇順へ(CSVが降順のことが多い)
    data = data.OrderBy(x => x.Date).ToList();

    return data;
}

ここまで整えると、CSV読み込みの失敗確率が大幅に下がります。特に昇順ソートは重要です。株価CSVは「新しい日付が先頭(降順)」で出てくることが多く、そのまま学習するとSSAが想定する“時間の流れ”と逆になります。

CSV列とC#型のおすすめ対応表

ML.NETや可視化まで考えるなら、まずはfloat/double中心に寄せるのが無難です。decimalを使いたい場合は「UI表示用にdecimal」「学習用にfloatへ変換」の二層に分けるのが安全です。

CSV列おすすめ型理由
Date28-Feb-24DateTimeラベル生成・昇順ソート・将来日付生成に必須
Open/High/Low/Close38138.35float(またはdouble)ML.NETが扱いやすい数値型。Chart.jsへ渡すのも容易
Shares Traded14799770float(またはdouble)学習に使わなくても、読み込み統一のためfloatで可
Turnover2511.43float(またはdouble)小数のためfloatが扱いやすい

ML.NET(Ssa)で例外が出る原因と対処

今回の報告では ForecastHighValues() を呼ぶと例外になり、呼ばなければCSV表示はできた、という状況でした。これは「CSVパースは直ったが、ML.NETの入力スキーマが成立していない」典型例です。

特に次の2つは頻出です。

  • decimalプロパティが混ざっている(IDataViewの型推論で詰まる)
  • SSAパラメータがデータ件数に対して大きすぎる(windowSize/seriesLength/trainSizeが不整合)

入力クラスと出力クラスは分ける

“入力データのクラス”に予測結果配列(float[] ForecastedHigh)まで同居させると、IDataViewスキーマが複雑になり、トラブルが増えます。ML.NETでは入力用予測出力用を分ける構成が基本です。

// 学習・推論に投入する最小の入力
public class HighInput
{
    public float High { get; set; }
}

// 予測結果(horizon分の配列が返る)
public class HighForecast
{
    public float[] ForecastedHigh { get; set; } = Array.Empty<float>();
    public float[] LowerBoundHigh { get; set; } = Array.Empty<float>();
    public float[] UpperBoundHigh { get; set; } = Array.Empty<float>();
}

株価CSVの行全体(Open/Closeなど)を持つクラスはUI用に残しつつ、ML.NETに渡す直前に HighInput へ射影(Select)するのが扱いやすい設計です。

SSAパラメータは“データ件数に合わせて小さく”始める

サンプルコードでありがちな次の設定は、実データが数十行しかないと高確率で破綻します。

windowSize: 115,
seriesLength: 200,
trainSize: 240,
horizon: 15

SSAは「学習に使う長さ(trainSize)」や「周期/パターンを見る長さ(seriesLength)」などが、投入データ数と整合している必要があります。まずは“動く最小”を作り、そこから徐々に調整するのが安全です。

パラメータ役割(ざっくり)データが少ないときの目安よくある失敗
trainSize学習に使うデータ件数基本は csvData.Countデータ件数より大きくして例外
windowSize局所パターンを見る窓幅5〜30程度(件数の1/3以下)大きすぎて学習不能
seriesLength周期性を見る長さwindowSize + 1以上、trainSize以下windowSize以上になっていない
horizon何ステップ先まで予測するか3〜15程度(件数が少ないなら小さく)未来を長くしすぎて不安定

動作確認用:データ件数から自動調整するForecastHighValues例

以下は「例外を出しにくくする」ことを優先した実装例です。まずはこれで動作を通し、精度改善は後で取り組む方が開発が止まりません。

using Microsoft.ML;
using Microsoft.ML.TimeSeries;

private HighForecast ForecastHighValues(List<StockData> csvData, int horizon)
{
    if (csvData == null || csvData.Count < 10)
        throw new InvalidOperationException("時系列予測には最低でも10件程度のデータが必要です。");

    // 念のため昇順
    var ordered = csvData.OrderBy(x => x.Date).ToList();

    var ml = new MLContext(seed: 1);

    // SSAは基本的に単変量(ここではHighのみ)
    var inputs = ordered.Select(x => new HighInput { High = x.High });
    var trainData = ml.Data.LoadFromEnumerable(inputs);

    int trainSize = ordered.Count;

    // まずは「小さめ」から。件数に応じて上限を設ける
    int windowSize = Math.Max(5, Math.Min(30, trainSize / 3));
    int seriesLength = Math.Max(windowSize + 1, Math.Min(trainSize, windowSize * 2));

    // horizonもデータ件数に対して暴れないように制限
    horizon = Math.Max(1, Math.Min(horizon, Math.Max(3, trainSize / 5)));

    var pipeline = ml.Forecasting.ForecastBySsa(
        outputColumnName: nameof(HighForecast.ForecastedHigh),
        inputColumnName: nameof(HighInput.High),
        windowSize: windowSize,
        seriesLength: seriesLength,
        trainSize: trainSize,
        horizon: horizon,
        confidenceLevel: 0.95f,
        confidenceLowerBoundColumn: nameof(HighForecast.LowerBoundHigh),
        confidenceUpperBoundColumn: nameof(HighForecast.UpperBoundHigh));

    var model = pipeline.Fit(trainData);

    var engine = model.CreateTimeSeriesEngine<HighInput, HighForecast>(ml);
    var forecast = engine.Predict();

    return forecast;
}

ここで重要なのは、decimalを入力に含めないこと、そしてパラメータをデータ件数に合わせて縮めることです。例外が解消されれば、次に“予測をどう描画するか”へ進めます。

注意:SSAは「将来のHighを正確に当てる」万能手法ではありません。株価は外的要因が強く、短いデータだと予測は不安定です。本記事はあくまで技術実装(可視化までの配線)の解説であり、投資判断を目的としません。

.NET MAUI(BlazorWebView)で「アップロード→予測→表→チャート」を通す実装例

ここからは、MAUI Blazorでよく採用される構成を前提に、動作する最小の流れを示します。ポイントは以下です。

  • アップロード直後にCSVをパースして、件数0なら画面に警告を出す
  • 予測は例外を握りつぶさず、メッセージを表示して原因切り分けしやすくする
  • チャート描画は OnAfterRenderAsync でJSInterop(DOMができてから)
  • 再描画時は既存チャートを破棄して二重生成を防ぐ

Razorコンポーネント例(CSV読み込み+予測+描画)

@page "/stocks"
@using System.Globalization
@inject IJSRuntime JS

CSV株価(High)をML.NETで予測してChart.jsで表示



@if (!string.IsNullOrWhiteSpace(_message))
{
@_message
}

@if (_rows.Count > 0)
{
読み込み結果(先頭20行)

@foreach (var r in _rows.Take(20))
{

}



Date
High



@r.Date.ToString("yyyy-MM-dd")
@r.High




<h3>チャート</h3>
<div style="max-width: 100%; overflow-x: auto;">
    <canvas id="stockHighChart" style="min-height: 320px;"></canvas>
</div>


}

@code {
private readonly List _rows = new();
private string _message = "";
private bool _shouldRenderChart;


// 予測結果
private string[] _labels = Array.Empty<string>();
private double?[] _actual = Array.Empty<double?>();
private double?[] _forecast = Array.Empty<double?>();
private double?[] _lower = Array.Empty<double?>();
private double?[] _upper = Array.Empty<double?>();

private async Task OnFileChanged(InputFileChangeEventArgs e)
{
    _message = "";
    _rows.Clear();
    _shouldRenderChart = false;

    try
    {
        var file = e.File;
        using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
        using var reader = new StreamReader(stream);
        var content = await reader.ReadToEndAsync();

        var parsed = ParseCsvRobust(content);

        if (parsed.Count == 0)
        {
            _message = "CSVを読み込めませんでした。日付形式や区切り、空白を確認してください。";
            return;
        }

        _rows.AddRange(parsed);

        // 予測(まずは小さめ)
        var horizon = 10;

        var forecast = ForecastHighValues(_rows, horizon);

        // ラベル(過去+未来)
        var futureDates = CreateFutureTradingDates(_rows.Last().Date, forecast.ForecastedHigh.Length);
        var allLabels = _rows.Select(x => x.Date.ToString("yyyy-MM-dd"))
            .Concat(futureDates.Select(d => d.ToString("yyyy-MM-dd")))
            .ToArray();

        // null埋めで「実績」と「予測」を同一軸に描く
        var total = allLabels.Length;

        var actual = new double?[total];
        for (int i = 0; i < _rows.Count; i++)
            actual[i] = _rows[i].High;

        var pred = new double?[total];
        var low = new double?[total];
        var up = new double?[total];

        for (int i = 0; i < forecast.ForecastedHigh.Length; i++)
        {
            int idx = _rows.Count + i;
            pred[idx] = forecast.ForecastedHigh[i];
            if (forecast.LowerBoundHigh.Length > i) low[idx] = forecast.LowerBoundHigh[i];
            if (forecast.UpperBoundHigh.Length > i) up[idx] = forecast.UpperBoundHigh[i];
        }

        _labels = allLabels;
        _actual = actual;
        _forecast = pred;
        _lower = low;
        _upper = up;

        _message = $"読み込み: {_rows.Count}行 / 予測: {forecast.ForecastedHigh.Length}ステップ";
        _shouldRenderChart = true;
    }
    catch (Exception ex)
    {
        _message = $"エラー: {ex.Message}";
    }
    finally
    {
        StateHasChanged();
    }
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (_shouldRenderChart)
    {
        _shouldRenderChart = false;

        await JS.InvokeVoidAsync(
            "renderStockHighChart",
            "stockHighChart",
            _labels,
            _actual,
            _forecast,
            _lower,
            _upper);
    }
}

private static List<DateTime> CreateFutureTradingDates(DateTime lastDate, int count)
{
    var dates = new List<DateTime>(count);
    var d = lastDate;

    while (dates.Count < count)
    {
        d = d.AddDays(1);

        // 土日スキップ(簡易版)
        if (d.DayOfWeek == DayOfWeek.Saturday || d.DayOfWeek == DayOfWeek.Sunday)
            continue;

        dates.Add(d);
    }

    return dates;
}


}

この例で大事なのは、チャート描画を「ファイル読み込み直後」ではなく、レンダリング後(OnAfterRender)に行う点です。DOM(canvas)が存在しないタイミングでJSを呼ぶと、描画関数が失敗して何も出ません。

Chart.js側(index.html / wwwrootに追加)

.NET MAUI Blazor(BlazorWebView)の場合、一般的には wwwroot/index.html にChart.jsの読み込みと描画関数を追加します。CDNを使う方法は手軽ですが、オフライン環境や社内ネットワークで制限がある場合は、Chart.jsをローカルに同梱する選択も検討してください。

<!-- wwwroot/index.html の末尾付近(bodyの終わり)に追加 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<script>
  // 既存チャートを破棄するために保持
  window.__stockHighChartInstance = window.__stockHighChartInstance || null;

  window.renderStockHighChart = function (canvasId, labels, actual, forecast, lower, upper) {
    const canvas = document.getElementById(canvasId);
    if (!canvas) return;

    const ctx = canvas.getContext("2d");

    // 二重生成防止
    if (window.__stockHighChartInstance) {
      window.__stockHighChartInstance.destroy();
      window.__stockHighChartInstance = null;
    }

    window.__stockHighChartInstance = new Chart(ctx, {
      type: "line",
      data: {
        labels: labels,
        datasets: [
          {
            label: "High(実績)",
            data: actual,
            spanGaps: false,
            tension: 0.2
          },
          {
            label: "High(予測)",
            data: forecast,
            spanGaps: false,
            borderDash: [6, 4],
            tension: 0.2
          },
          {
            label: "下限(参考)",
            data: lower,
            spanGaps: false,
            borderDash: [2, 3],
            tension: 0.2
          },
          {
            label: "上限(参考)",
            data: upper,
            spanGaps: false,
            borderDash: [2, 3],
            tension: 0.2
          }
        ]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        interaction: { mode: "index", intersect: false },
        plugins: {
          legend: { display: true }
        },
        scales: {
          x: { ticks: { maxRotation: 0 } },
          y: { beginAtZero: false }
        }
      }
    });
  };
</script>

ここでは、C#側から double?[](null可)を渡し、Chart.js側でnullが入っている区間は線が途切れるようにしています。これにより「過去は実績」「未来は予測」を同一のX軸(ラベル)で自然に表現できます。

「予測値の反映」でつまずかないための考え方

よくある誤解が、「予測配列(horizon分)を、既存のcsvDataの先頭から順番に上書きする」やり方です。SSAの予測は“未来のステップ”なので、基本は最後の日付の次からに配置します。

今回のように、予測値を次のように割り当ててしまうと、未来ではなく過去の行に入ってしまいます。

// これは「未来」ではなく先頭から上書きしがち
for (int i = 0; i < forecastedValues.Length; i++)
{
    if (csvData.Count > i)
    {
        csvData[i].ForecastedHigh = new[] { forecastedValues[i] };
    }
}

おすすめは、記事内の実装例のように「ラベルを過去+未来に拡張し、配列をnull埋めして描画する」方式です。UI表示もシンプルになります。

表現したいものデータ構造メリット
過去の実績(High)labelsの先頭側に実値を入れるCSV表示と同じ並びで整合が取りやすい
未来の予測(High)labelsの末尾側に予測値を入れる“予測=未来”が視覚的に明確
同一チャートで2系列表示実績は未来側null、予測は過去側nullChart.jsで自然に線が分離する

チャートが表示されないときのチェックリスト

CSVとML.NETの問題を直したのにチャートが出ない場合は、ここから先は“BlazorとJSInteropの作法”の話です。次の順で確認すると、短時間で潰せます。

チェック項目見るべき症状対処
labels/actual/forecastの長さ配列長が0、または不一致パース件数0を疑う/配列生成ロジックを見直す
canvasのidが一致しているかJS側でcanvasがnullRazor側のidとJS引数を一致させる
JS関数が読み込まれているか「renderStockHighChart is not a function」index.htmlのscript順序を確認(Chart.js→自作関数)
呼び出しタイミング初回だけ出ない/更新で出ないOnAfterRenderで呼ぶ/更新時フラグで再描画
チャート二重生成2回目以降で動かない、重なる既存インスタンスdestroyしてから生成

MAUI Blazorでのデバッグを楽にする設定

Windows上の.NET MAUI(BlazorWebView)なら、開発時にDevTools相当を有効化しておくと、JSエラーがすぐ見つかります。代表的には、デバッグビルドでDeveloper Toolsを追加するパターンです(プロジェクトテンプレートにより記述場所は異なります)。

// MauiProgram.cs の例(DEBUG時のみ)
builder.Services.AddMauiBlazorWebView();

#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
#endif

これでコンソールエラー(関数未定義、CDNブロック、canvas取得失敗など)が見えるようになり、チャート“だけ”が出ない問題の切り分けが速くなります。

よくある「もう一段の落とし穴」:データが少なすぎる・欠損が多い

CSVが10〜20行程度だと、SSAは動いても予測が暴れたり、信頼区間が極端になったりします。技術的に“動く”と“使える精度”は別なので、UI上は次のようなガードを入れておくと親切です。

  • データ件数が少ない場合は予測ボタンを無効化(例:30件未満は警告)
  • 欠損行(パース失敗行)が多い場合は失敗件数を表示
  • 予測値は“参考”ラベルを付け、実績と区別する

また、株価は週末・祝日で欠損が発生します。SSAは「1ステップ=1行」として扱うので、日付の間隔が不均一でも“次の取引日”として解釈すればよい一方、将来日付を生成するときは簡易的に土日を飛ばすだけでも、視覚的な違和感が減ります(祝日まで厳密にやる場合は市場カレンダーが必要です)。

まとめ:最短で直すための優先順位

  • 最優先:CSVの読み込み件数が0になっていないか確認し、日付フォーマットを dd-MMM-yy に合わせる
  • 次:ML.NETは入力をfloat/doubleに統一し、入力クラスと出力クラスを分離する
  • 次:SSAパラメータはデータ件数に合わせて小さく始め、まず例外を出さずに予測配列を得る
  • 最後:チャートはOnAfterRenderでJSInteropし、配列をnull埋めして実績/予測を同一軸に描く

この順番で潰していけば、「CSV → 予測 → Chart.js表示」の一連が止まらずに通る構成になります。動作が安定したら、次の段階として“予測対象をHigh以外にも広げる”“パラメータを検証データでチューニングする”“予測と実績の誤差を別チャートで可視化する”など、品質改善にも取り組みやすくなります。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次