.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/trainSize | float/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列 | 例 | おすすめ型 | 理由 |
|---|---|---|---|
| Date | 28-Feb-24 | DateTime | ラベル生成・昇順ソート・将来日付生成に必須 |
| Open/High/Low/Close | 38138.35 | float(またはdouble) | ML.NETが扱いやすい数値型。Chart.jsへ渡すのも容易 |
| Shares Traded | 14799770 | float(またはdouble) | 学習に使わなくても、読み込み統一のためfloatで可 |
| Turnover | 2511.43 | float(または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、予測は過去側null | Chart.jsで自然に線が分離する |
チャートが表示されないときのチェックリスト
CSVとML.NETの問題を直したのにチャートが出ない場合は、ここから先は“BlazorとJSInteropの作法”の話です。次の順で確認すると、短時間で潰せます。
| チェック項目 | 見るべき症状 | 対処 |
|---|---|---|
| labels/actual/forecastの長さ | 配列長が0、または不一致 | パース件数0を疑う/配列生成ロジックを見直す |
| canvasのidが一致しているか | JS側でcanvasがnull | Razor側の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以外にも広げる”“パラメータを検証データでチューニングする”“予測と実績の誤差を別チャートで可視化する”など、品質改善にも取り組みやすくなります。

コメント