データの作成
出力の元になるデータは、プログラムに直接書かないで、別ファイルで管理しましょう。[プロジェクト]メニューの[新しい項目の追加]で[テキストファイル]を選び、ファイル名を「list.txt」にして[追加]ボタンを押してください。追加するときは、ソリューションエクスプローラで[CZ成分解析]を選択しておいてください。「list.txt」の中身は、例えば次のようにしてください(プサイさんのサイトから頂きました)。
下心 微妙さ 優雅さ 華麗さ かわいさ やさしさ やましさ やらしさ むなしさ ツンデレ 厳しさ 世の無常さ ハッタリ ビタミン 努力 気合 根性 砂糖 食塩 愛 電波 毒電波 元気玉 怨念 大阪のおいしい水 明太子 勇気 運 電力 小麦粉 汗と涙(化合物) 覚悟 大人の都合 見栄 欲望 嘘 真空 呪詛 信念 夢 記憶 鉄の意志 カルシウム 魔法 希望 不思議 勢い 度胸 乙女心 罠 花崗岩 宇宙の意思 犠牲 毒物 鉛 海水 蛇の抜け殻 波動 純金 情報 知識 知恵 魂の炎 媚び 保存料 着色料 税金 歌 苦労 柳の樹皮 睡眠薬 スライム アルコール 時間 果物 玉露 利益 赤い何か 白い何か 鍛錬 月の光 回路 野望 陰謀 雪の結晶 株 黒インク 白インク カテキン 祝福 気の迷い マイナスイオン 濃硫酸 ミスリル お菓子 言葉 心の壁 成功の鍵 理論 血
「list.txt」を選んで、プロパティのビルドアクションを[埋め込まれたリソース]に変えてください。こうすることにより、このデータが実行ファイルに埋め込まれ、「list.txt」がなくても実行できるようになります。
イベントハンドラの作成
以下の機能を実装します。
- MainFormの
Loadイベント(初期表示時の処理) - btnAnalyzeの
Clickイベント([解析]ボタンを押したときの処理)
usingの指定
「MainForm.cs」の最初に、以下を追加してください。「MainForm.cs」を開くには、ソリューションエクスプローラで「MainForm.cs」を右クリックして、[コードの表示]を選んでください。
using System.IO;
MainFormのLoadイベント
成分のリストは、初期表示時に作成します。初期表示時は、MainFormのLoadイベントが呼ばれます。MainFormのデザイン画面でMainFormをダブルクリックしてください(コントロールをダブルクリックしないようにしてください)。MainForm_Loadを次のように修正します。
private string[] list; private void MainForm_Load(object sender, EventArgs e) { try { using (StreamReader sr = new StreamReader(GetType().Assembly. GetManifestResourceStream("CZ成分解析.list.txt"), Encoding.Default)) { list = sr.ReadToEnd().Split('\n'); } } catch (FileNotFoundException) { list = new string[] { "???" }; } }
メンバ変数listは、成分の全文字列が入ります。
btnAnalyzeのClickイベント
MainFormのデザイン画面でbtnAnalyzeをダブルクリックしてください。btnAnalyze_Clickを次のように修正します。
private void btnAnalyze_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(tbInput.Text)) { MessageBox.Show("成分を入力してください"); return; } int seed = tbInput.Text.GetHashCode(); // 乱数の元 // 毎日結果が変わるように seed ^= DateTime.Now.DayOfYear; Random rand = new Random(seed); // 乱数 int[] index = new int[list.Length]; // ランダムな順番 for (int i = 0; i < index.Length; ++i) index[i] = i; Algo.RandomShuffle(rand, index); // ランダムシャッフル // 結果用 List<Pair<int, double>> res = new List<Pair<int, double>>(); int n = Math.Max(1, Math.Min(index.Length, (int)(4 + NormalRandom(rand)))); for (int pos = 0; pos < n; ++pos) { double d = 1.0 / (pos + 1) * Math.Exp(3 + NormalRandom(rand)); res.Add(new Pair<int,double>(index[pos], d)); } res.Sort(delegate(Pair<int, double> pr1, Pair<int, double> pr2) { return -pr1.Second.CompareTo(pr2.Second); }); double dsum = 0; foreach (Pair<int, double> pr in res) dsum += pr.Second; StringBuilder sb = new StringBuilder(tbInput.Text + "の成分解析結果 :\r\n"); foreach (Pair<int, double> pr in res) sb.Append(string.Format("{0}の{1}%は{2}で出来ています。\r\n", tbInput.Text, (int)(pr.Second * 100 / dsum), list[pr.First])); tbResult.Text = sb.ToString(); } private double NormalRandom(Random rand) { return rand.NextDouble() + rand.NextDouble() + rand.NextDouble() + rand.NextDouble() + rand.NextDouble() + rand.NextDouble() + rand.NextDouble() + rand.NextDouble() + rand.NextDouble() + rand.NextDouble() + rand.NextDouble() + rand.NextDouble() - 6; }
成分解析のメインの処理になります。
- 入力文字列が空でないかをチェックします。
- 入力文字列から
GetHashCodeを使って、シードを計算します。 - 入力文字列が同じなら同じ値となること
- 入力文字列が異なれば、なるべく違う値となること
- シードから乱数を作成します。同じシードを与えると、乱数は同じように発生します。
- 乱数を利用して、添え字の配列をランダムシャッフルします。この添え字の順番に、成分が表示されます。
- 成分の数を決めます。平均4個、標準偏差1個の正規分布に従う乱数を切り捨てて使いましょう。正規分布は、-∞から∞の値となる可能性があるため、1からリスト数の間に入るように調整します。
- 正規分布に従う乱数は、通常はボックスミューラー法などを利用して発生させます。ここでは、単純に12個の一様乱数を足して6を引いて発生させます。この方法は、簡単なプログラムで(ほぼ正しい)正規分布を発生させることができます。
- 各成分ごとに割合を決定します。割合の平均がジップの法則に従うにように1.0/(pos+1)を基準にしましょう。また、ランダムになるように乱数をかけます。正規分布を用いると負の値になる可能性があるため、ここでは負の値をとらない対数正規分布を用います。対数正規分布は正規分布の指数関数をとることで得られます。
- 一般的な分布の乱数は、元になる分布の確率密度関数の累積関数の逆関数に対して、0~1の一様乱数を適用することで得られます。
- 発生させた割合の大きい順にソートします。C# 2.0ではAnonymous Methodを用いることで、複雑なソートも簡易に記述できるようになっています。
- 発生した割合からパーセンテージを計算できるように、割合の総和を求めます。
- 成分を計算し、結果を表示します。
実行
- [ビルド]メニューの[ソリューションのビルド]を行います
- [デバッグ]メニューの[デバッグなしで開始]を選んで実行します
- 文字を入力して[Enter]キーを押してください
まとめ
以下の演習を行いました。
- 2つの値を扱う
Pair構造体の使用 - ランダムな順列の生成方法
- 文字列から特定の整数値を作成する方法
- 簡単な標準正規分布の乱数と、対数正規分布の乱数の作成方法
