ClosedXMLを使って、超高速にExcelからSQL Serverへデータ転送する

SQL Serverのデータをバックアップするとき正式な方法ならば、SQL Serverのバックアップ機能を使ってバックアップ/復元をすればよい。のだが、別な場所のデータベースに移したいとき使うにはいささか面倒くさいことが起こる。相互のバージョンが同じならば、そのあっまバックアップ/復元を繰り返せばよいのだが、ちょっとでもバージョンが違うとエラーになってしまう。仕方がないので、Accessを媒介する方法をよくとるのだが、型が微妙に違っているので(特に文字列の長さが異なってエラーになる)若干の手動の調節が必要になってくる。MySQLならば、ダンプコマンドを使ってえいっとSQLに直してしまうところなのだが、SQL Serverにはそんなものはない。

Excelを媒介にしてみる

適当なバックアップツールを作るのもよいのだが、ここは Excel を使って媒介してみよう、と考えた。SQL Server Management Studioにはデータのエクスポート先に、AccessやExcelを選ぶことができる。当然、Excel からインポートすることもできるのだが、ID の設定でちょっと手動なところがある。できることならば、一発でインポートしたいところだ。それに、Access じゃなく Excel でインポートできれば、ちょっとした修正ならば Excel で修正した後にインポートし直すということができる。いわゆるマスターテーブルの修正を、Excel で書き込んで、SQL Serverに書き戻すことができれば便利だろう、と思たたわけなのだが。

試しに Microsoft.Office.Interop.Excel を使って Cells で参照しながらちまちま読み込んで、SqlBulkCopy を使って一気にインポートすれば高速になるだろう、と思ったのだが…ああ、とてつもなく遅い。どうも COM 経由で Cells を参照しているところがむちゃくちゃ遅くて、1000件位の読み込みでも5,6分はかかってしまうという体たらくだ。たった1万件でも1時間程度かかってしまう見込みなので、これはちょっと実用に耐えない。
ちなみに、Range を使って二次元配列に読み込んでという方法を使うと高速化できるのだが、ちょっとセルをアクセスをすると遅くなるのと、デバッグ実行などで COM な Excel が残ったりして結構面倒なことが多い。
Range を二次元配列で読み取る方法は、後日示しておきたい。手軽な方法ではあるので。

ClosedXML に切り替える

XML形式で保存される xlsx のほうならば、Open XMLが使える。どうやら ClosedXML は内部的にOpen XML SDK を使っているそうなので、これを使ってみる。生の OpenXML を使うよりも断然 ClosedXML のほうが使いやすいという記事があるので、参考にさせてもらう。

【C#】Excelの取り扱いにClosedXMLを使用する – あたも技術ブログ
http://atamo-dev.hatenablog.com/entry/2017/07/23/180026

ClosedXMLを使った、Excel操作の例 ClosedXML
http://closedxml.codeplex.com/ https://gist.github.com/ishisaka/6128639

果たしてどれだけスピードアップするのか?と思って試してみたのだが、体感的には Microsoft.Office.Interop.Excel より ClosedXML のほうが100倍位早い。最初に xlsx ファイルを開くときには結構時間が掛かる(と言っても10秒位)。おそらく XML ファイル全てを読み込みパースするので時間が掛かっているのだろうけど、全体でかかる時間からすれば全然大したことはない。

ClosedXML で読み込んで SqlBulkCopy する

Excel から書き戻すのが目的なので、INSERTにはSqlBulkCopyを使う。SqlBulkCopyに渡すのはDataTableになるので、EFのエンティティクラス(単純な値クラス)からDataTableオブジェクトに変換した関数を使う。

NuGet で ClosedXML を取り込んで、XLWorkbook クラスで WorkBook を読み込む。

using ClosedXML.Excel;

private void clickReadClosedXml(object sender, RoutedEventArgs e)
{
    // 0.テーブル名を指定
    string tableName = "営業業務データ";
    // 1.Excel を開く
    string path = @"営業管理_20190201.xlsx";
    using (var wb = new XLWorkbook(path))
    {
        var sh = wb.Worksheets.FirstOrDefault(t => t.Name == tableName);
        // 2.シートからEFに読み込み
        var rc = new ClosedXmlRangeConverter<営業業務データ>(sh);
        var items = rc.ToList();
        // 3.データベースの「営業業務データ」に書き込み
        var ent = new testdbEntities();
        var dt = items.AsDataTable();
        var cn = ent.Database.Connection as SqlConnection;
        var bc = new SqlBulkCopy(cn);
        bc.DestinationTableName = tableName;
        cn.Open();
        bc.WriteToServer(dt);
        cn.Close();
        MessageBox.Show(&quot;データを保存しました&quot;);
    }
}

SSMS から Excel にエクスポートするとシート毎にテーブルのバックアップが作られる。これを wb.Worksheets.FirstOrDefault で見つける。Excel からセルを読み込んで List に返す自作のコンバーター ClosedXmlRangeConverter を作っている。
List を DataTable に書き直す拡張メソッド AsDataTable を作っておいて SqlBulkCopyのWriteToServer に渡す。

ちなみに、変換元の営業業務データクラスは、EFでデータベースから自動生成させたモデルクラスをそのまま使っている。

public partial class 営業業務データ
{
    public int 業務番号 { get; set; }
    public Nullable<int> 支店コード { get; set; }
    public string 支店名 { get; set; }
    public Nullable<int> 受注番号年度 { get; set; }
    public Nullable<int> 受注番号 { get; set; }
    public Nullable<int> 受注番号枝番 { get; set; }
    public Nullable<int> 未契約番号年度 { get; set; }
    public string 未契約番号 { get; set; }
    public string 社内業務番号 { get; set; }
    public Nullable<int> 営業担当者コード { get; set; }
    public string 営業担当者名 { get; set; }
    public Nullable<int> 発注者コード { get; set; }
    public string 発注者名 { get; set; }
    public string 発注番号 { get; set; }
	...

こんな風なテーブルになっている。

?IXLCellの拡張メソッド

IXLCell 自体は GetValue() で型変換ができるのだが、今回はエンティティクラスの各プロパティの型に合わせるので、リフレクションのPropertyInfoクラスで対象の型に変換できるように拡張メソッドを作っている。

/// CloseXmlのIXLCellの拡張メソッド
public static class ClosedXmlCellExtensions
{
    public static int ToInt(this IXLCell cell)
    {
        if (cell.Value == null) return 0;
        int result = 0;
        return int.TryParse(cell.Value.ToString(), out result) ? result : 0;
    }
    public static int? ToNullableInt(this IXLCell cell)
    {
        if (cell.Value == null) return (int?)null;
        int result = 0;
        return int.TryParse(cell.Value.ToString(), out result) ? result : (int?)null;
    }
    public static double ToDouble(this IXLCell cell)
    {
        if (cell.Value == null) return 0.0;
        double result = 0.0;
        return double.TryParse(cell.Value.ToString(), out result) ? result : 0.0;
    }
    public static double? ToNullableDouble(this IXLCell cell)
    {
        if (cell.Value == null) return (double?)null;
        double result = 0.0;
        return double.TryParse(cell.Value.ToString(), out result) ? result : (double?)null;
    }
    public static decimal ToDecimal(this IXLCell cell)
    {
        if (cell.Value == null) return 0;
        decimal result = 0;
        return decimal.TryParse(cell.Value.ToString(), out result) ? result : 0;
    }
    public static decimal? ToNullableDecimal(this IXLCell cell)
    {
        if (cell.Value == null) return (decimal?)null;
        decimal result = 0;
        return decimal.TryParse(cell.Value.ToString(), out result) ? result : (decimal?)null;
    }
    public static DateTime ToDateTime(this IXLCell cell)
    {
        if (cell.Value == null) return new DateTime();
        DateTime result = new DateTime();
        return DateTime.TryParse(cell.Value.ToString(), out result) ? result : new DateTime();
    }
    public static DateTime? ToNullableDateTime(this IXLCell cell)
    {
        if (cell.Value == null) return (DateTime?)null;
        DateTime result = new DateTime();
        return DateTime.TryParse(cell.Value.ToString(), out result) ? result : (DateTime?)null;
    }
    public static bool ToBoolean(this IXLCell cell)
    {
        if (cell.Value == null) return false;
        bool result = false;
        return bool.TryParse(cell.Value.ToString(), out result) ? result : false;
    }
    public static bool? ToNullableBoolean(this IXLCell cell)
    {
        if (cell.Value == null) return (bool?)null;
        bool result = false;
        return bool.TryParse(cell.Value.ToString(), out result) ? result : (bool?)null;
    }
    public static string ToText(this IXLCell cell)
    {
        if (cell.Value == null) return "";
        return cell.Value.ToString();
    }

    public static object To(this IXLCell cell, System.Reflection.PropertyInfo pi)
    {
        var pt = pi.PropertyType;
        if (pt == typeof(int)) return cell.ToInt();
        if (pt == typeof(int?)) return cell.ToNullableInt();
        if (pt == typeof(double)) return cell.ToDouble();
        if (pt == typeof(double?)) return cell.ToNullableDouble();
        if (pt == typeof(bool)) return cell.ToBoolean();
        if (pt == typeof(bool?)) return cell.ToNullableBoolean();
        if (pt == typeof(DateTime)) return cell.ToDateTime();
        if (pt == typeof(DateTime?)) return cell.ToNullableDateTime();
        if (pt == typeof(string)) return cell.ToText();

        return null;
    }
}

コンバーター ClosedXmlRangeConverter クラス

IXLWorksheet の内容を読み込んで、目的のエンティティクラスのリストを作るためのコンバーターを作成する。シートの1行目にテーブルの列名が入っている想定で作ってある。

/// RangeからEFへのコンバーター
/// Entity Frameworkで利用するエンティティクラスに読み込む
/// 主に ClosedXmlのExcel からデータを読み込み、EF でデータベースに出力するときに使う
public class ClosedXmlRangeConverter<T> where T : class, new()
{
    protected List<System.Reflection.PropertyInfo> _Columns = new List<System.Reflection.PropertyInfo>();
    protected IXLWorksheet _sh;
    /// コンバーターの作成
    public ClosedXmlRangeConverter(IXLWorksheet sh)
    {
        _sh = sh;
        // 最初の行をコンバート先のテーブルと照合する
        var props = typeof(T).GetProperties();
        int col = 1;
        while (sh.Cell(1, col).Value.ToString() != &quot;&quot;)
        {
            var text = sh.Cell(1, col).Value.ToString();
            var prop = props.FirstOrDefault(t => t.Name == text);
            if (prop != null)
            {
                _Columns.Add(prop);
            }
            col++;
        }
    }

    /// 行単位でコンバート
    public T ToItem(int row)
    {
        var item = new T();
        for (int col = 0; col < this._Columns.Count; col++)
        {
            var prop = _Columns[col];
            var o = _sh.Cell(row, col + 1).To(prop);
            prop.SetValue(item, o);
        }
        return item;
    }
    /// 全てのデータをコンバート
    public List<T> ToList()
    {
        var items = new List<T>();
        int r = 2;
        while (_sh.Cell(r, 1).GetValue<string>() != &quot;&quot;)
        {
            var item = this.ToItem(r);
            items.Add(item);
            r++;
        }
        return items;
    }
}

エンティティクラスをDataTableに変換する拡張メソッド

DbSet から DataTable を作成するための AsDataTable() を作る。そのままでは、DataRow の型に Nullable が入らないので、DBNull への切り替えを行っている。

/// 
<summary>
/// DbSetをDataTableに変換
/// </summary>

public static class DataTableExtenstions
{
    public static DataTable AsDataTable<T>(this DbSet<T> src) where T : class
    {
        return DataTableExtenstions.AsDataTable(src.Local);
    }
    public static DataTable AsDataTable<T>(this IEnumerable<T> src) where T : class
    {
        var properties = typeof(T).GetProperties();
        var dest = new DataTable();
        // テーブルレイアウトの作成
        foreach (var prop in properties)
        {
            if ( prop.PropertyType.IsGenericType == true &amp;&amp;
                    prop.PropertyType.GetGenericTypeDefinition().Name == &quot;Nullable`1&quot;)
            {
                /// Nullable<int>のときは、
                /// DataRowの中身を DBNull と int の時に分けなければいけない。
                var originalType = prop.PropertyType.GetProperty(&quot;Value&quot;).PropertyType;
                var column = new DataColumn();
                column.DataType = originalType;
                column.AllowDBNull = true;
                column.ColumnName = prop.Name;
                dest.Columns.Add(column);
            }
            else
            {
                dest.Columns.Add(prop.Name, prop.PropertyType);
            }
        }
        // 値の投げ込み
        foreach (var item in src)
        {
            var row = dest.NewRow();
            foreach (var prop in properties)
            {
                var itemValue = prop.GetValue(item, new object[] { });
                row[prop.Name] = itemValue ?? System.DBNull.Value;
            }
            dest.Rows.Add(row);
        }
        return dest;
    }
}

計測はあとで

これで Excel シートから SQL Server への書き戻しが高速に行える。全てのデータを書きこ戻してしまうので、ピンポイントで修正することはできないがマスターテーブルの書き換えとかを Excel 上で行って、SQL Server に戻すことができれば結構便利だと思う。修正自体もお客さんに行って貰うこともできそうだし。

スピードは、COM 経由よりも100倍位早いのだが、これは後で実測してみる。

カテゴリー: 開発 | ClosedXMLを使って、超高速にExcelからSQL Serverへデータ転送する はコメントを受け付けていません

LINQ の INSERT を SqlBulkCopy にするとどれだけ早くなるのか?

昨日書いたばかりの、これだけど、SqlBulkCopy を使うとどれだけ早くなるのかを再び実験

LINQ の INSERT が遅いときは AutoDetectChangesEnabled を False にする
http://www.moonmile.net/blog/archives/9646

結論

結論から言えば、SqlBulkCopy を使うほうが100倍位早いですね。1万件位だと6秒から0.06秒という誤差?っぽい感じだけど、100万件になるとLINQのINSERTでは難しいので、SqlBulkCopy を直接使えという感じです。

SqlBulkCopy は DataTable を受け取る

SqlBulkCopy Class (System.Data.SqlClient) | Microsoft Docs
https://docs.microsoft.com/ja-jp/dotnet/api/system.data.sqlclient.sqlbulkcopy?view=netframework-4.7.2

SQL Server専用の SqlBulkCopy な訳ですが、引数に DataTable か DataRow の配列を取ります。いわゆるEFじゃない DataSet/DataTable のものを使わないといけないので、EFのDbSetがそのまま渡せません。
逆に言えば、EFのDbSetを渡せるようにDataTableに変換してやれば、LINQとSqlBulkCopyが共存可能になります。

任意のオブジェクトの配列をDataTableに変換する – Qiita
https://qiita.com/keidrumfreak/items/f092b3cacfc2961610b6

この記事を参考にしながら、というかそのまま使って、AsDataTable という拡張メソッドを作ります。

public static class DataTableExtenstions
{
    public static DataTable AsDataTable<T>(this DbSet<T> src) where T : class
    {
        return DataTableExtenstions.AsDataTable( src.Local );
    }
    public static DataTable AsDataTable<T>(this IEnumerable<T> src) where T : class
    {
        var properties = typeof(T).GetProperties();
        var dest = new DataTable();
        // テーブルレイアウトの作成
        foreach (var prop in properties)
        {
            dest.Columns.Add(prop.Name, prop.PropertyType);
        }
        // 値の投げ込み
        foreach (var item in src)
        {
            var row = dest.NewRow();
            foreach (var prop in properties)
            {
                var itemValue = prop.GetValue(item, new object[] { });
                row[prop.Name] = itemValue;
            }
            dest.Rows.Add(row);
        }
        return dest;
    }
}

EFのDbSetは内部でLocalプロパティ(データベースに反映する前のデータを取っている持っておくコレクション)があるので、これを利用して DataTable に変換します。

実験

SqlBulkCopy に渡すデータを List で用意してから DataTable に変換するパターン

private void clickBulk(object sender, RoutedEventArgs e)
{
    var ent = new testdbEntities();
    ent.Database.ExecuteSqlCommand(&quot;delete BulkT&quot;);
    var start = DateTime.Now;
    var lst = new List<BulkT>();
    for (int i = 0; i < 10000; i++)
    {
        var t = new BulkT()
        {
            GUID = Guid.NewGuid().ToString(&quot;N&quot;),
            Created = DateTime.Now,
        };
        lst.Add(t);
    }
    var dt = lst.AsDataTable();
    var cn = ent.Database.Connection as SqlConnection;
    var bc = new SqlBulkCopy(cn);
    bc.DestinationTableName = &quot;BulkT&quot;;
    cn.Open();
    bc.WriteToServer(dt);
    cn.Close();

    var tend = DateTime.Now;
    var span = (tend - start).TotalSeconds;
    System.Diagnostics.Debug.WriteLine(span.ToString());
}

SqlBulkCopy に渡すデータをLINQのAddで追加してから DataTable に変換するパターン

private void clickAsDataTable(object sender, RoutedEventArgs e)
{
    var ent = new testdbEntities();
    ent.Configuration.AutoDetectChangesEnabled = false;
    ent.Configuration.ValidateOnSaveEnabled = false;
    ent.Database.ExecuteSqlCommand(&quot;delete BulkT&quot;);
    var start = DateTime.Now;
    for (int i = 0; i < 10000; i++)
    {
        var t = new BulkT()
        {
            GUID = Guid.NewGuid().ToString(&quot;N&quot;),
            Created = DateTime.Now,
        };
        ent.BulkT.Add(t);
    }
    var cn = ent.Database.Connection as SqlConnection;
    var bc = new SqlBulkCopy(cn);
    bc.DestinationTableName = &quot;BulkT&quot;;
    var dt = ent.BulkT.AsDataTable();
    cn.Open();
    bc.WriteToServer(dt);
    cn.Close();

    var tend = DateTime.Now;
    var span = (tend - start).TotalSeconds;
    System.Diagnostics.Debug.WriteLine(span.ToString());
}

結果

List を使って DataTable に変換
0.0477976
0.0400377
0.034159

LINQのAddを使って DataTable に変換
1.7919409
1.7825315
1.7763977

AutoDetectChangesEnable = false の場合
6.311641
5.8724151
6.0832671

単純に比較すると、AutoDetectChangesEnable を false にしただけよりも、SqlBulkCopy を使ったほうが6倍位早くなります。さらに、EFのDbSetを使わずに、Listだけを使った場合は、25倍位早くなるってことですね。どうやら、DbSetに対してAdd/Removeしたときに DetectChanges() 等のチェックルーチンが走るらしく、単純な大量 INSERT の場合には SqlBulkCopy を直接使ってしまたほうが早いです。

カテゴリー: 開発, C# | LINQ の INSERT を SqlBulkCopy にするとどれだけ早くなるのか? はコメントを受け付けていません

LINQ の INSERT が遅いときは AutoDetectChangesEnabled を False にする

とあるシステムで1万件程度のテーブルを構成しなおして別の複数のテーブルに移すことをやっていた。いわゆる、正規化していないテーブルをデータ移行の際に正規化しようと思って、複数のテーブルに分けたのだが、たかだか1万件しかないのに非常に遅い。正規化のロジックが遅いのかもしれないけど、1万件程度をデータ挿入するのに30分位掛かってしまうのである。
相手が SQL Server なので SqlBulkCopy を使えば結構なスピードになるはずなのだが、100万件の場合ならばそうかもしれないけど、たかだか1万件の挿入でこんなに遅いのは変。ということで、LINQ の INSERT について調べなおしてみる。

結論

結論から言えば、AutoDetectChangesEnabled と ValidateOnSaveEnabled を OFF(false) にすればよい。

DbContextConfiguration.AutoDetectChangesEnabled Property
https://docs.microsoft.com/en-us/dotnet/api/system.data.entity.infrastructure.dbcontextconfiguration.autodetectchangesenabled?redirectedfrom=MSDN&view=entity-framework-6.2.0#overloads

ent.Configuration.AutoDetectChangesEnabled = false;
ent.Configuration.ValidateOnSaveEnabled = false;

LINQ で INSERT/DELETE/UPDATE をする場合、EF の内部で整合性をチェックしている。System.Data.Entity.DbSet.Add などを呼び出したときに、DetectChanges() でチェックをしているというわけだ。どうやらこれが遅い原因なので、素直に System.Data.Entity.Infrastructure.DbContextConfiguration.AutoDetectChangesEnabled の値を false にして呼び出さないようにすればよい。
デフォルトでは、AutoDetectChangesEnabled が true となっている。

実験

CREATE TABLE [dbo].[BulkT](
	[ID] [int] IDENTITY(1,1) NOT NULL,
	[GUID] [varchar](100) NOT NULL,
	[Created] [datetime] NOT NULL
)

この BulkT テーブルに1万件のデータを挿入する。SaveChanges を1回の挿入ごとに行っているが、テーブルが複雑な場合は最後に1回だけだとメモリを食いすぎたりするため、何度かに分ける(100回毎など)必要がある。

private void clickNormal(object sender, RoutedEventArgs e)
{
    var ent = new testdbEntities();
    ent.Database.ExecuteSqlCommand(&quot;delete BulkT&quot;);
    var start = DateTime.Now;
    for ( int i=0; i<10000; i++ )
    {
        var t = new BulkT()
        {
            GUID = Guid.NewGuid().ToString(&quot;N&quot;),
            Created = DateTime.Now,
        };
        ent.BulkT.Add(t);
        ent.SaveChanges();
    }
    var tend = DateTime.Now;
    var span = (tend - start).TotalSeconds;
    System.Diagnostics.Debug.WriteLine(span.ToString());
}

private void clickNoAutoDetect(object sender, RoutedEventArgs e)
{
    var ent = new testdbEntities();
    ent.Database.ExecuteSqlCommand(&quot;delete BulkT&quot;);
    var start = DateTime.Now;
    ent.Configuration.AutoDetectChangesEnabled = false;
    ent.Configuration.ValidateOnSaveEnabled = false;
    for (int i = 0; i < 10000; i++)
    {
        var t = new BulkT()
        {
            GUID = Guid.NewGuid().ToString(&quot;N&quot;),
            Created = DateTime.Now,
        };
        ent.BulkT.Add(t);
        ent.SaveChanges();
    }
    var tend = DateTime.Now;
    var span = (tend - start).TotalSeconds;
    System.Diagnostics.Debug.WriteLine(span.ToString());
}

DetectChanges のチェックあり(AutoDetectChangesEnabled = true)
50.1799167
48.9856614
48.5278795

DetectChanges のチェック無し(AutoDetectChangesEnabled = false)
6.1251368
6.1039912
6.2945836

このように8倍ぐらいの差がでてくる。100件程度ならば特に問題もでないだろうが、1万件以上ある場合は気を付けたおいたほうがよいだろう。データチェックが入らないので、挿入データに気を付ける必要があるが、今回のように空のテーブルに挿入する場合はプログラム内でチェックが済んでいるので特に問題はない。

ちなみに、30分以上掛かっていたデータコンバートは1分以内に終わるようになった。

参考リンク

Entity Framework のパフォーマンス #2 更新処理 | C#.NET vs VB.NET
http://csharpvbcomparer.blogspot.com/2015/04/net-ef-performance-2-updating.html

カテゴリー: 開発, C# | LINQ の INSERT が遅いときは AutoDetectChangesEnabled を False にする はコメントを受け付けていません

もっと簡単に超概算見積もりver2

以前、超概算見積もり を考えたのはすでに10年前になるので、この際だからバージョンアップ版をあげておこう。この超概算見積もりの手法は PMBOK で言うところの超概算見積もりとは違う。だが、おそらく現実に即しているはずだ。

ざっくり規模見積もりをしたいときに、機能から見積もり始めるのは間違いだ。特に受託開発の場合には、営業さんの云うところの「ざっくり見積もる」というのは、予算と期間になる。いわゆる、QCD で言うところのコストと期限(Delivery)になる。

このため、各種の見積もり手法では「規模見積もり」→「予算見積もり」→「スケジュール」の順番で見積もりをしていくのだが、実際のところ、

  • 予算が限られている(Cost)
  • リリース日などの期限が決まっている(Delivery)

ことが多いので、Cost と Delivery の制約を先に埋めてしまうほうがよい。

予算(コスト)が決まっている場合

お客や営業さんから「ざっくり予算はいくらでしょうか?」と聞かれることが多いだろうが、言っている人の中にはすでに「ざっくりとした予算」があることが多い。だから、「ざっくりとした予算は?」と聞かれたならば、「あれとこれとこれを組み合わせたら、最大で1億円ですね」と答えるとよい。当然「1億円なんて予算はないよ」という顔をするから(億円単位の大規模開発ならば別だろうけど)、「あれとこれを削って、ここだけ作れば100万円ですかね」と、あきらかに機能不足な話の低予算を提示するとよい。そうすると「そんなに機能が少ないんじゃあだめで、これとこれぐらいはないと」という回答が得られる。

つまり、最大限と最小限の間に「予算」は決まっているのは確かなことなのだ。

その間の部分で、仮決めで(相手の懐を予想しながら、あるいは直接「予算はどれくらいでしょうか?」と聞いても良い)、500万円ぐらいの予算で、と決めたとしよう。

人月計算をやりやすくするため単価100万円/月のソフトウェア開発者を割り当てるとすると、単純計算する超概算見積もりで「予算は500万円、開発期間は5か月」というプロジェクトが生まれる。開発期間は5か月が上限なので、これを超えるとプロジェクトは赤字だ。赤字なプロジェクトは受けてはいけない案件である(時と場合によるけど)。

これを自社で行っているソフトウェア開発手法で見積もってみる。

  • スクラムのスプリントを使った場合、スプリントが2週間であれば10スプリント
  • チケット駆動で、3チケット/日とすれば、3x20x5 = 300 チケット
  • WBS やタスクで週平均3程度を想定すれば、20/3 x 5 = 33 WBS

このように スプリント/チケット/WBS/タスク の上限が決まってくる。

チケットの内容をすべて埋めなくても良い。ある程度チケットの内容を埋めてしまえば、

  • 予算以内におさまりそうか(チケットが枯渇しなさそうか)
  • 予算以上になりそうか(チケットが大幅に上回るだろうか)

ということがわかる。超過する場合には、500万円という予算を優先して(お客がそれしか出せないのだから仕方がない)、

  • 機能(チケット)を減らす → 何か盛り込みすぎている
  • 単価を下げる → 並行プロジェクトにより、スケジュールに余裕を持たせる。薄く引き伸ばす

ことになる。うすく引き伸ばす方法は、並行プロジェクトを使ったリスク分散の方法だ。これはまた別の機会に話す。

スケジュール(期限)が決まっている場合

年度末までに開発を終えるとか、元号対応だとか別の理由でスケジュールが決まっている場合がある。このときも「ざっくりと予算と期間はどのくらいですか?」と聞かれるのだが、期限は後ろに倒せない。

例えば、半年後にリリースが決まっている6か月のプロジェクトを想定しよう。すると、単価100万円/月の場合は、600万円の予算となる。ここからチケット数を決めて、概算で割り振ってみる方法は、予算優先の場合と同じだ。

期限内におさまりそうにない(必要なチケットの数が予想よりも多い)場合は、

  • 人数を増やす → 単純に馬力を増加させる。当然、予算は増加する。
  • 機能を減らす → 期限優先なので、優先度が低い機能は落としてしまう。

リリース日を後ろ倒しにできないので、保険(2割増しあるいは5割増し)を入れておいてリリース日に確実に間に合うようにすることを忘れずに。

予算(コスト)や期限(スケジュール)で概算する

最初から機能(FP法やCOCOMO2など)を使って予算や期間を見積もりしてしまと、結局のところお客が想定している予算や期限と食い違いがでてしまって「機能」自体の見積もりしなおしになる。だから、予算や期限から機能の方を逆算して、規模見積もりが分かったところから再び予算や期間を見積もり直せばよい。特に、客相手の受託開発の場合にはここがポイントになる。

プロジェクト実行時の再見積もり

超概算見積もりをした後に再チェックをして、予算や期間ができたとしよう。このときの見積もりの根拠は「機能」がベースになっているので、プロジェクト実行時にこの機能(規模見積もり)が正しいかどうかを再チェックする。

さきに書いた通り、受託開発では予算と期限が優先なのだから、プロジェクト実行時に注目するポイントは以下になる。

  • 機能が増加していないか?
    → チケットが増えていないか?規模見積もりの前提条件がずれていないかをチェックする。
  • チケットの消化数は適切か?
    → バーンダウンチャートでチケットを消化する傾きをチェックする。
    → 期限に間に合うか?
  • 並行プロジェクトがある場合は、適切にチケットが消化されていないか?
    → 他方のプロジェクトに押されていないか?
    → 仕事の時間が増えていないか。生産効率が落ちていないか?

チケットの消化数を守るために、残業や休出をして補っている場合、勤務時間が増えているのだから「生産効率が落ちている」状態になる。生産量(この場合は消化するチケット数)は同じで、時間が掛かりすぎているのだから、仕事の効率が落ちているとみる。

これらをプロジェクト実行時にはチェックして、対処(人を増やす、予算を増やす、期限をずらす、機能を減らす)を行うのがプロジェクトマネジメントである。

計測のための数え上げに関しては、デマルコ氏の進捗管理やマコネル氏のWBS等の数え上げによる見積もり手法を参考にするとよい。

カテゴリー: OpenCCPM | もっと簡単に超概算見積もりver2 はコメントを受け付けていません

プロジェクトマネジメントの工学的アプローチのメモ書き

ソフトウェア開発におけるプロジェクトマネジメント手法を工学的にアプローチするメモを流しておこう。ちなみに、この記事には「モチベーション」という言葉は出てこないし「スーパープログラマ」も「カリスママネージャー」も出てこない。あくまで工学/自然科学的なアプローチで解決をしようという話である。ある意味「働き方改革」も出てこない。

前提条件

「プロジェクト」とは何かというところからスタートするほどでもないが、この手法には前提条件がある。

  • WBS/チケット/タスクが全て消化されれば、プロジェクトが終了する

という条件が必要になる。一見当たり前のように見えるけど、研究プロジェクトや目的達成型のプロジェクトの場合にはこのマネジメント手法では無理がある。研究プロジェクトは数々の試行錯誤が必要なので、チケットに対する時間効果が判別つかない。また一定の目的を達成する(製品販売数とかユーザー数目標とか)プロジェクトの場合も、達成するまでにいろいろな試行錯誤が出てくるので、形式的なプロジェクトマネジメント手法は向かない。この場合は、(おそらく)イノベーションマネジメントになると思う。こっちの方はまた別の機会に書くことにする。

WBSあるいはチケットあるいはタスクは、何らかの仕事の「粒」となる粒度ともいう。面倒なので以後「チケット」で用語を統一するが、それぞれの違いは

  • PMBOK的にトップダウンで仕事を出す場合は「WBS」
  • ボトムアップ的に仕事を出す場合は「タスク」
  • チケット駆動の場合は「チケット」

ということになるが、ひとまとめにして「チケット」と呼ぶ。
このチケットの数や大きさ(作業期間や外注金額など)などは、プロジェクト開始以降から変化してもよいものとする。PMBOKの場合は事前にWBSを出し切る要請があったりするが、ここでは後からWBSを追加可能とする。タスクの場合も同じ。あとから追加要望とか見落としとかでタスクが増える場合も考えられる。

主に受託案件を考える

適用範囲は主に受託案件を考える。社内製品開発でもよいのだが、社内の場合はもう少し制約が緩い場合が多いので、厳しいほうで試してみるのがよいだろう。受託案件の場合、顧客から予算と期間が区切られることが多く、単純なアジャイル開発による「交渉」機能がうまく働かないことが場合が多い。大幅に機能が増加されていれば予算枠を増やす交渉をすることも可能なのだが、ちょっとした機能追加とか受託側の都合による機能追加/見落としなどでの「仕事」の追加では追加予算が出ないのが普通だ。よって、

「チケット」が増えたにも関わらず、「予算」や「納期」が動かないことが多い。

むしろこちら側であるはずの「営業サイド」から値下げ要求や期間短縮、コストダウンなどを要求される場合が多いというのが受託開発でのソフトウェア開発での特徴だ。
この場合「マネージャー」もあちら側に回ることもできるのだが、今回はこちら側に回ることにする。あちら側にまわるやり方は色々出ているので試してみるといい。お勧めはしないが。

これにより、受託開発で案件を受けるときの条件として

  • 開発期間、納期が決まっている。

→ よって、無理なスケジュールを強制されたときは、最初から拒否する。

  • 予算の上限がある。

→ よって、無理な予算を強制されたときは、最初から拒否する。

  • 開発すべき機能が決まっている。

→ よって、無理な機能追加/拡張が要求されたときは、最初から拒否する。

ということにする。「無理な」というのは、あきらかに開発期間が短かったり、低予算すぎたり、夢のような機能がてんこもり、という場合のことだ。最初から失敗するプロジェクトから逃げるというのは有能なマネージャやリーダーの鉄則だ。もちろん、あちら側に回ればそういう案件を受けることも可能であるのだが、それは(以下略

さて、達成できそうな受託案件が目の前にできたとしよう。「達成できそうな受託案件」というのは、ほどよく顧客からの要望がでてきて、QCD(機能、コスト、納期)の概算ができたときに限るという訳だ。ここで問題になるところだが、これは2つの問題を内包している。

  • 受託開発案件における妥当なQCDをどうやって出すのか?
  • 妥当なQCDの開発プロジェクトを「成功」させるためには、どうしたらよいのか?

まるで、鶏と卵の問題のようだが、この2つは同時に解決する。というか同時に解決しないといけない。

計画と工程管理を一度に検証する

具体的な手順を示そう。

1.初期の条件として「無理のない」QCD を考える。ここは初期値なので、勘で適当に決めて良い。あるいは、顧客の状態を考えて、ざっくりと予算と納期(開発期間)を決めてしまうのがよい。よく営業さんが言う「ざっくりと」で良い。

2.社内で開発するチームの「チケット」の消費スピードを決める。理想は1チケット2,3時間程度なのだが、1チケットが1日でもよい。1日以上のチケットを考えるとプロジェクト実行時に誤差が多くなるので、1日以内におさめたほうがよい。

例えば、1日3チケット、期間が半年、メンバが5人と考えれば、
3x20x6x5=1800チケット
ということになる。

3.総チケットのうち、20%から30%ぐらいが「予備」のチケットとなる。予備というのは不確定要素を吸収するためのチケットで、プロジェクトを進行したときの変更要求とか機能が膨らんだときの仕事を吸収するためのチケットである。20%から30%ってのは経験上のもので、これはよく言われる「プロジェクトの予算を1.2倍から1.5倍するとよい」と同じことになる。いわゆる逆数だ。0.2/1.2 ≒ 17%、0.5/1.5 ≒ 33% となる。

残りの仕事のためのチケットを使って1で見積もったときの機能実装などを割り振る。WBS的にトップダウンで割り振ってもよいし、ボトムアップ的にタスクとして割り振ってもよい。それぞれの期間が揃っているほうがいいのだが、あまり気にしなくてもよい。重要なのは最初から割り振られているチケットの数になる。最初から割り振るチケットは「必ずやらなければいけない仕事」なので、優先度が高い。
ここでチケットが足りなくなったり、予備チケットを使い始めたらダメだ。最初の1の「ざっくり開発期間」が間違っていることになる。ざっくりの部分をざっくりと直して、2のチケット数を増やしていく。

逆に、すべての仕事チケットを埋める必要はない。概算の概算として設計工程/実装工程/試験工程で3つの山に分けてしまって、設計工程のチケットだけ書いてみるという方法でもよい。空白チケットがあってもよい。

計画段階ではこれでおしまい。ここで、おおむねチケット駆動のシミュレーションがおわってる。

  • トータルのチケット数(予備を含む)
  • 開発期間
  • 人数

が分かったので、1日(あるいは1週間)で消化するチケット数が計算できる。これが「生産量」になる。

4.プロジェクトが進行している間は順次チケットを消化する。チケット駆動のツールを使ってもよいし、ひとりプロジェクトならば Excel だけでもよい。
仕事チケットを日々消化すると同時に、予期しない仕事があれば予備チケットを使う。顧客からの要望が増えれば予備チケットを使う。営業からの要求があれば予備チケットを使う。増えたら予備チケットを使う。

仕事チケットが大幅に膨らんでしまった場合は、複数の予備チケットに分割する。当然、予備チケットではない空白チケットがあればそれを使う。考慮不足を補うのが空白チケットや予備チケットの役目だ。空白チケットはもともと予定されているけど内容が書かれなかったもの(面倒だったとか、書く時間がなかったとか)、予備チケットはいわゆる「プロジェクトバッファ」分のチケットのことになる。

5.進捗管理(工程管理)は、実際のチケット消化数と3で計算した事前のチケット消化状態と比較する。順調であれば傾き(バーンダウンチャート)は同じぐらいになる。実際の消化数が多い場合は特に問題はない。ちょっとぐらい遅れていても問題はない。総チケットの上に行かなければよい、かつバーンダウンチャートの予想直線を引いたときに総チケットのラインよりも傾きが浅くならなければよい。

予備チケットの分だけ保険が入っているので、進捗はこの間をうろうろすることになる。
バーンダウンチャートを引いたときに、赤い線より上に出そうであれば、プロジェクト進行を見直すことになる。
「生産量」を上げることはできないので(突如として「生産量」があがるという幻想は捨て去るべきだ)、

  • プロジェクトメンバを増やす
  • 仕事チケットを減らす(開発機能を減らす)
  • 仕事をする時間を増やす=休日出勤

ことになる。当然、開発予算を増やす交渉や納期を後ろに倒す交渉もし始める。休日出勤は緊急避難的なのでカンフル剤としてか使えない。一瞬しか進捗は良くならない。受託案件の場合、予算や納期が変わらないことが多いので、先の2点が対処になるだろう。

以上の5ステップで、計画と進捗管理はおしまい。

利点

このプロジェクト管理での利点は、先に書いた通りメンバのモチベーションとかやる気とかコミュニケーションとかの不確定要素が一切出てこない、それに頼らないことだ。それらはチーム開発では必要かもしれないが、必ずしも高度なものである必要はない。一般的にできれば十分だ。少なくとも、受託案件のような場合には「標準的な生産量」の測定が重要になるので、不確定要素に頼らないほうがよい。

欠点

これは利点と同時に欠点でもあるのだが、この方式にはミクロな視点が入らない。チケット同士の関連やなんらかの要因による「生産量」(チケットの消化スピード)の低下を予測できない。スケジュール管理自体がメンバ任せになってしまうので、効率の悪いチケットの消化をしてしまう可能性がある。

当然のことながら、規模が大きくなる場合は、

  • チケット同士の関連性(PERT図)
  • マイルストーン(ガントチャート)

が必要になってくる。この2つの図はおいおい活用していくとして、ガントチャートは「チケットの増加に素早く対応できない」という大きな欠点がある。チケットやタスク、WBSを増やしたときにガントチャートを修正するのが難しいため「ガントチャートが直せないから、チケットを増やさずにガントチャートのスケジュール通りに行動する」という本末転倒な心理が働いてしまう。これでは駄目だ。これはツールによる制約があるので OpenCCPM で解消していきたい点である。

おわりに

メモなのでまとめはいらないのだが、作業見積もりの20%増しとか50%増しとかで顧客に提出するとき、その保険が削られて営業サイドで勝手に値引きされてしまうことがある。お客の前で「値引き」すると案件が取りやすいからね。営業テクニックである。仕方がない。なので、見積もりを出す側(マネージャでもリーダーでも誰でも良い)では営業の値引きが明らかになっている場合は、あらかじめ「値引き」の値段だけ予算に加えておけばよい。値引き自体に根拠はないのだから。

また、予備チケットや保険の分が「顧客」に理解されないときは、計画段階や進捗管理で「二重帳簿」を作るとよい。二重帳簿は Excel などを使って自動計算すると簡単に作れる。

  • 顧客や営業にとって安心されやすい「進捗」グラフ
  • プロジェクトメンバがリスク管理できる「進捗」グラフ

を二種類用意するわけだ。超概算見積もりも同じパターンで作ることができる。その話は別の機会に。

カテゴリー: 開発, OpenCCPM | プロジェクトマネジメントの工学的アプローチのメモ書き はコメントを受け付けていません

読み取り専用の値オブジェクトがWPFのTextBoxにバインドされない(ように見える)件

最終的に、WPFのバグを踏んだのか!?と思ったけど、よく見ればきちんと例外が出ていたという話なので、今後注意するという備忘録的な記事です。

現象

こんな風な値オブジェクトを作っておいて、WPFのウィンドウにバインドします。

public class ViewModel
{
    public int ID { get; set; }
    public string Name { get; set; }
    public string NameSan { get => Name + &quot;-san&quot;; } 
}

NameSan プロパティは加工して表示するだけの読み取り専用のプロパティです。金額の合計値を出すとか、なにか計算結果をだすとかそういう ReadOnly な表示はよくやるパターンですね。

ViewModel _vm;
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    _vm = new ViewModel();
    _vm.ID = 100;
    _vm.Name = "Masuda";
    this.DataContext = _vm;
}

ここで NameSan プロパティを表示するときに、TextBlock タグを使えばよいのですが、TextBox を使って ReadOnly=”True” にします。TextBox を使う理由としては、表示している文字列のコピーが Ctrl-C でできるからです。けれど、読み取りにしたいから、ReadOnly を付けておきます。

<TextBox Grid.Column=&quot;1&quot; Grid.Row=&quot;0&quot; Text=&quot;{Binding ID}&quot; />
<TextBox Grid.Column=&quot;1&quot; Grid.Row=&quot;1&quot; Text=&quot;{Binding Name}&quot; />
<TextBox Grid.Column=&quot;1&quot; Grid.Row=&quot;2&quot; Text=&quot;{Binding NameSan}&quot; IsReadOnly=&quot;True&quot; />

これをビルドして動かそうとすると実行時に例外が発生します。

System.InvalidOperationException
HResult=0x80131509
Message=TwoWay または OneWayToSource バインドは、型 ‘WpfApp4.ViewModel’ の読み取り専用プロパティ ‘NameSan’ では動作できません。
Source=PresentationFramework

どうやら、TextBox の Text プロパティは読み書き同時にできることが前提となっているので NameSan のように get しかない読み取り専用プロパティではエラーが発生するのです。IsReadOnly を付けていても駄目ですね。これは、WPFのModeがデフォルトで「TwoWay」になっており双方向になっているのが原因です。ここを明示的に「Mode=OneWay」にして表示のみにすると実行時エラーはでなくなります。

<TextBox Grid.Column=&quot;1&quot; Grid.Row=&quot;0&quot; Text=&quot;{Binding ID}&quot; />
<TextBox Grid.Column=&quot;1&quot; Grid.Row=&quot;1&quot; Text=&quot;{Binding Name}&quot; />
<TextBox Grid.Column=&quot;1&quot; Grid.Row=&quot;2&quot; Text=&quot;{Binding NameSan, Mode=OneWay}&quot; IsReadOnly=&quot;True&quot; />

ちなみに、UWPの場合はデフォルトが「OneWay」になっているので動作が違います。これがややこしい。

実験

あらかじめ、この現象を知っておけば Mode=OneWay をちまちまつけるのですが、どうやら手元でたくさんサブウィンドウを作っているときにうっかり OneWay をつけ忘れたのです。

すると、サブ画面ではこんな風に NameSan プロパティをバインドしている TextBox の初期値がでなくなります。

サブ画面を開くときは、こんな風に ViewModel を渡して(本当はコンストラクタのほうがいいけど)、ShowDialogメソッドで開きます。

private void clickOpen(object sender, RoutedEventArgs e)
{
    var sub = new SubWindow();
    sub.VM = _vm;
    sub.ShowDialog();
}

実行に落ちることはなく、単にバインドに失敗します。

これ、実行時の出力を注意深くみると、実は「例外」が発生しています。

メイン画面のときに失敗したとき同じように、バインド時に例外が発生しているのですが、ShowDialogメソッド内で例外を握りつぶしているので、初期値が表示できない(バインドできない)現象だけが出てサブウィンドウ自体は表示されるという現象です。

所感

EF で作った自動生成のオブジェクトは get/set が揃っているので、この現象は発生しません。LINQ で読み込んだリストとかをグリッドにバインドするとか TextBox にバインドしても get/set があるので助かるのだけど。
計算結果の表示とか、なんらかの文字列を加工した後に表示するときに get だけの読み取り専用プロパティを作ったときにはまります。しかも、これ TextBlock の場合はもともと読み取り専用(表示専用)なので代用部なんだけど、TextBox のような読み書き用のコントールだけに当てはまる現象だという、結構レアなケースですが、覚えておくとハマりにくい穴かもしれません。

カテゴリー: 開発, C#, WPF | 読み取り専用の値オブジェクトがWPFのTextBoxにバインドされない(ように見える)件 はコメントを受け付けていません

PINE64 と Xamarin.Forms の組み合わせを考える

この記事は、Xamarin Advent Calendar 2018 17日目の記事です。

PINE64 ってのは 3,000 円級で買えるラズパイ互換の組み込みボードです。ラズパイ互換というといささか語弊があるのですが、メモリが1GB タイプならば秋月で買えるし Raspberry Pi 3 Model B よりも高機能なところがあります。

image

Android 7.1 を入れる

最初に PINE64 を買ってみたのは Windows IoT Core を動作させたいためだったのですが、最初の頃は LAN などが上手く動かなくて諦め状態だったのです。今のバージョンは試していないので、それはまた今度。

でも、Android がほどよく動きます。Raspberry Pi 3B に Android を入れるとちょっと重たくて使い物にならないのですが、PINE64 に入れるとまずまずのスピードで動くようになります。さすがにアクションゲームは難しいのですが、カードゲームっぽい RPG ものならば大丈夫だし、この記事のように Xamarin.Forms のようなテキストであれば十分なスピードがでます。このあたり GPU が乗っているのが大きいとは思うのですが、詳しいことは不明。

それでも、普通の Android スマホとはちょっと変わった形で Android が使えるのがミソです。

デメリットの解説を

先に PINE64 + Android の組み合わせのデメリットの話をしておきましょう。

  • ラズパイより情報が少ないので構築に苦労する
    → ふつうに Ubuntu で使うにしても苦労するし、まあ差額考えてもラズパイのほうが楽
  • タッチパネルが使えない
    → そもそもモニタがないので、別途液晶モニタがないと何もできないです。
    → USB にマウスを付けないと使えないです
  • バッテリーがないので、電源を引っこ抜くと消える
    → 電源を USB 経由で供給しないと駄目なので、引っこ抜くと Android ともども落ちます。モバイルバッテリーだといけるかもしれません。
    → Android を動かすのに、5V2A が必要です。これはパソコンからの USB 給電では動きません。

あと、Android から GPIO を操作するのは root が必要で微妙に面倒です。面倒というかまだ試していないので、試さないと。すでに買ってから1年以上経っていますが。

ちょっとだけメリットを

実運用するにはデメリットが多いのですが、ちょっとだけメリットがあります。

  • 液晶モニタを HDMI 接続できる
  • 超安価な Android 機として扱える。
    → 中古でも1万円するので、2GB タイプでも 5,000円弱です。
    → Android 7.1 が用意されています。SDK からビルドすれば 8.1 も動きそうな気がするのですが試していません。
  • Android を microSD として入れられる。
    → 初期データを入れたり、データをコピーしたりできます。
    → 最初から root です。
  • Google Play が使えます。
    → PINE64 + Android では Google Play が使えます。

そんなこんなで試しに動かしてみると面白いかもしれません。Android SDK から自前ビルドをして動作させることができるので、それなりにハックはできるんですが、SDK のビルド環境を整えるのが結構大変で。もともと構築済みの人だといいんだと思うのだけど、初手からやるのはなかなか辛いかもしれません。それだけのメリットが得られるのか?というとかなり疑問で、だったらふつうにラズパイ+Raspbian で動かしたほうが無難かも、とおもったりもしますが。

それでも、実験的に PINE64 + Android + Xamrin.Forms というのをたまに試してみたりします。

環境構築

環境構築をざっと書いておきます。

  • PINE A64-LTS ボードを購入。左上の Store から直販で $32 で買えます
  • microSD のイメージは SOPINE Software Release – PINE64 の Android 7.1 Community Build Image をダウンロードする。
  • PINE64 + Android 7.1 を起動したら WiFi ADB を入れて起動。Android 側の adbd が起動して、PC から adb connect できるようになります。

adb から PINE64+Android で接続できるようになったら、準備完了です。

image

ちなみに、半年前に動かしたアズレンがこれです。今やキャッシュが壊れているのか画像がうまくでないんですが。再構築したら出るかも。

image

Xamarin.Forms を動かしてみよう

Android が動くのだから、Xamarin.Android も動くだろうし、Xamarin.Forms も動くだろうとう話です。Xamarin.Forms のメリットは、Android, iOS の両方のスマホを一気に作れるというものが大きいのと、もうひとつ C# でさっくりとした画面をさっくりと作れるというメリットがあります。私が Android IDE に慣れていないからというせいもあるかもしれませんが、スマホのような小さな画面ではなくて液晶モニタのように大きな画面でレイアウトを作る場合は、Xamarin.Forms で使われている Grid が効率的です。

なので、大き目のタブレット画面 + Android という組み合わせで業務アプリを作る場合に、Xamarin.Forms の Grid と MVVM がベストだろう、と考えているわけですが…世の中、大き目の Android タブレットという動きにはならなかったんですよね。今後はどうなんでしょう?

それはさておき、Xamarin.Forms のサンプルを動かしたのがこれです。

image

image

これ自体は、Visual Studio で Xamarin.Forms のテンプレートを使っているだけなので、説明は省略。

image

image

これだけだといまいちなので、github から xamarin.forms のサンプルを動かしてみましょう。

xamarin-forms-samples/CatClock
https://github.com/xamarin/xamarin-forms-samples/tree/master/CatClock

image

ほかのサンプルも

image

image

image

元が Android なので、UI はそのまま XAML を使えます。スマホの場合には画面が小さいので全画面で作ることになるのですが、23インチ液晶位になるともう少しレイアウトを考える必要がありますよね。そのあたりにうまくハマるのではにかな、と考えています。

カテゴリー: Android, Xamarin | PINE64 と Xamarin.Forms の組み合わせを考える はコメントを受け付けていません

M5Stack から Slack に Yo! する

この記事は、M5Stack Advent Calendar 2018 – Qiita  の 10日目の記事です。
以前つくってみた M5Stack から Slack へ投稿する記事の焼き直しなのですが、もう少し詳しく書きます。というか、以前作ったやつをコピペしたら動かないということで、ちょっと書き直し。

仕組み

Slack へ投稿する方法として、Web APIを使って直接投稿することもできるのですが、なかなか面倒ので WebHook を使います。HTTP で手軽にアクセスできる API を用意しておいてアクセスする方法ですね。WebHook 内で複雑なアクセスを肩代わりしてくれるので、組み込み側に専用のライブラリを用意する必要がなくなります。

というわけで、

  • SlackのWebHookを作る
  • 作った WebHook に HTTPS で送る

2手順で実現します。

SlackのWebHookを作る

ブラウザで slack にログインしている状態で左上の「Manage」をクリックします。

検索ボックスで「WebHook」を入力して「Incoming WebHooks」を選択。

「Add Configuration」をクリックして WebHook を作ります。

投稿先のチャンネルを指定して、WebHook の URL を作成します。

投稿するデータは JSON 形式で送信して、宛先の URL が https://hooks.slack.com/services/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 な感じになります(このURL自体はすでに無効にしてあるので大丈夫)。

投稿するのがテキストだけであれば、次のように text だけに設定

{"text": "This is a line of text in a channel.\nAnd this is another line of text."}

投稿時のアイコンやユーザー名も指定できます。実はチャネル名も指定できるので、WebHook 自体はチャンネルごとに作らなくてもひとつだけ作っておけばOKです。

{
 "text":"From M5Stack Yo!",
 "icon_emoji":":ghost:",
 "username":"m5stackpost"
}

これで WebHook の準備は完了

証明書をハードコードする

色々調べると M5Stackから HTTP 接続する場合に HTTPClient を使うのですが、Slack の WebHook は HTTPS 接続になっています。セキュリティ接続になる場合、公開鍵の証明書が必要になってきます。

実は例のうんこボタンやエリクソンの証明書の期限切れの問題もここに関わる訳ですが、証明書の使い方が「暗号化」と「ライセンス期限」との2つを混在させてしまっている問題があります。この部分、暗号化だけ使うのであれば有効期限は無視してよいし、ライセンス期限の制限だけなれば証明書の有効期限だけをチェックすればよいわけです。

さて、今回は Slack のサーバーとの暗号化通信となるので、サーバー側の証明書を M5Stack 側に埋め込んで暗号化できるようにします。

Google Chrome で slack のサイトを開いた状態でサーバーの証明書を表示させます。

「証明書のパス」のタブを開いてルート証明書か中間証明書を選択して「証明書の表示」をさせます。どちらの証明書を使っても構いません。

証明書をテキストファイルに保存します。

C++ のコードに埋め込んでしまうので、BASE64 形式で保存します。

中身をメモ帳で開くと、こんな感じになっています。

この証明書を M5Slack のコードに埋め込んで、HTTPS 接続するときに使います。

M5Stackのコード

Slack に Yo するコードはこんな感じです。
WiFi に接続するための SSID とパスワードは適宜変えてください。
Slack に送るための WebHook のところ「/services/XXXXX/YYYYY」も書き換えます。

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <HTTPClient.h>
#include <M5Stack.h>

WiFiMulti wifi;
HTTPClient http;

#define WIFI_SSID "<WiFiのSSID>"
#define WIFI_PASS "<WiFiのパスワード>"
const char *server = "hooks.slack.com";
const char *json = "{\"text\":\"From M5Stack Yo!\",\"icon_emoji\":\":ghost:\",\"username\":\"m5stackpost\"}";
const char *json2 = "{\"text\":\"From M5Stack Hello!!!\",\"icon_emoji\":\":ghost:\",\"username\":\"m5stackpost\"}";

// ルート証明書
const char* slack_root_ca= \
"-----BEGIN CERTIFICATE-----\n" \
"MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh\n" \
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" \
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\n" \
"QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT\n" \
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n" \
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG\n" \
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB\n" \
"CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97\n" \
"nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt\n" \
"43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P\n" \
"T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4\n" \
"gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO\n" \
"BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR\n" \
"TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw\n" \
"DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr\n" \
"hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg\n" \
"06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF\n" \
"PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls\n" \
"YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk\n" \
"CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=\n" \
"-----END CERTIFICATE-----\n" ;
  
// 中間証明書
const char * slack_sub_ca = 
"-----BEGIN CERTIFICATE-----\n" 
"MIIElDCCA3ygAwIBAgIQAf2j627KdciIQ4tyS8+8kTANBgkqhkiG9w0BAQsFADBh\n" 
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" 
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\n" 
"QTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaME0xCzAJBgNVBAYTAlVT\n" 
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJzAlBgNVBAMTHkRpZ2lDZXJ0IFNIQTIg\n" 
"U2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\n" 
"ANyuWJBNwcQwFZA1W248ghX1LFy949v/cUP6ZCWA1O4Yok3wZtAKc24RmDYXZK83\n" 
"nf36QYSvx6+M/hpzTc8zl5CilodTgyu5pnVILR1WN3vaMTIa16yrBvSqXUu3R0bd\n" 
"KpPDkC55gIDvEwRqFDu1m5K+wgdlTvza/P96rtxcflUxDOg5B6TXvi/TC2rSsd9f\n" 
"/ld0Uzs1gN2ujkSYs58O09rg1/RrKatEp0tYhG2SS4HD2nOLEpdIkARFdRrdNzGX\n" 
"kujNVA075ME/OV4uuPNcfhCOhkEAjUVmR7ChZc6gqikJTvOX6+guqw9ypzAO+sf0\n" 
"/RR3w6RbKFfCs/mC/bdFWJsCAwEAAaOCAVowggFWMBIGA1UdEwEB/wQIMAYBAf8C\n" 
"AQAwDgYDVR0PAQH/BAQDAgGGMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYY\n" 
"aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6\n" 
"Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RDQS5jcmwwN6A1\n" 
"oDOGMWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RD\n" 
"QS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v\n" 
"d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHQYDVR0OBBYEFA+AYRyCMWHVLyjnjUY4tCzh\n" 
"xtniMB8GA1UdIwQYMBaAFAPeUDVW0Uy7ZvCj4hsbw5eyPdFVMA0GCSqGSIb3DQEB\n" 
"CwUAA4IBAQAjPt9L0jFCpbZ+QlwaRMxp0Wi0XUvgBCFsS+JtzLHgl4+mUwnNqipl\n" 
"5TlPHoOlblyYoiQm5vuh7ZPHLgLGTUq/sELfeNqzqPlt/yGFUzZgTHbO7Djc1lGA\n" 
"8MXW5dRNJ2Srm8c+cftIl7gzbckTB+6WohsYFfZcTEDts8Ls/3HB40f/1LkAtDdC\n" 
"2iDJ6m6K7hQGrn2iWZiIqBtvLfTyyRRfJs8sjX7tN8Cp1Tm5gr8ZDOo0rwAhaPit\n" 
"c+LJMto4JQtV05od8GiG7S5BNO98pVAdvzr508EIDObtHopYJeS4d60tbvVS3bR0\n" 
"j6tJLp07kzQoH3jOlOrHvdPJbRzeXDLz\n" 
"-----END CERTIFICATE-----\n" ;

void init() {
  M5.begin(true, false, true);
  M5.Lcd.clear(BLACK);
  M5.Lcd.setTextColor(YELLOW);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(65, 10);
  M5.Lcd.println("Slack example");
  M5.Lcd.setCursor(3, 35);
  M5.Lcd.println("Press A button");
}

void setup() {
  init();
}

void sendYo() 
{
  // put your setup code here, to run once:
  wifi.addAP(WIFI_SSID, WIFI_PASS);
  M5.begin();
  M5.Lcd.println("send slack");
  
  while (wifi.run() != WL_CONNECTED) {
    delay(500);
    M5.Lcd.printf(".");
  }
  M5.Lcd.println("wifi connect ok");
  // post slack
  // https://hooks.slack.com/services/XXXXX/YYYYY
  M5.Lcd.println("connect slack.com");
  http.begin( server, 443, "/services/XXXXX/YYYYY", slack_sub_ca );
  http.addHeader("Content-Type", "application/json" );
  http.POST((uint8_t*)json, strlen(json));
  M5.Lcd.println("post hooks.slack.com");
}

void sendYa() 
{
  // put your setup code here, to run once:
  wifi.addAP(WIFI_SSID, WIFI_PASS);
  M5.begin();
  M5.Lcd.println("send slack");
  
  while (wifi.run() != WL_CONNECTED) {
    delay(500);
    M5.Lcd.printf(".");
  }
  M5.Lcd.println("wifi connect ok");
  // post slack
  // https://hooks.slack.com/services/XXXXX/YYYYY
  M5.Lcd.println("connect slack.com");
  http.begin( server, 443, "/services/XXXXX/YYYYY", slack_sub_ca );
  http.addHeader("Content-Type", "application/json" );
  http.POST((uint8_t*)json2, strlen(json2));
  M5.Lcd.println("post hooks.slack.com");
}

void loop() {
  M5.update();
  if (M5.BtnA.wasReleased()) {
    sendYo();
  } else if (M5.BtnB.wasReleased()) {
    sendYa();
  } else if (M5.BtnC.wasReleased()) {
    init();
  }
}

Aボタンで「Yo」を送って、Bボタンで「Yha!」を送ります。
Cボタンで画面クリアですね。

いちいち WiFi にログインしているのが悪いのか(たぶん接続を切っていないから、WiFiMulti がクローズされていないのだと思う)、ボタンを2回押さないと Yo が送れません。そのあたりは今後改良ということで。

実行結果

M5Stack の表示

Slack クライアントの表示

カテゴリー: 開発 | M5Stack から Slack に Yo! する はコメントを受け付けていません

SQL Server を SUM/AVG でオーバーフローさせてみよう

ちょっと誤差関係で気になったことを試してみる。

統計学では平均は分散を使うことが多いのだけど、標本の数が多い場合にデータベースの sum や avg は有効に働くのだろうか?という疑問がでてくる。単純に言えば、SQL Server などのデータベースで扱える数値はどのくらい大きいあるいは精度が出るのだろうか?

以下は、SQL Server 2016 で試した結果

計算結果は38桁まで有効

単純に有効数値を確認するためにはオーバーフローさせればよい。例の0.01をどこまで掛けられるかを「0」を打ちながら試してみる。

こんな感じで、入力するときに38桁でエラーになる。

同じように、計算結果が38桁を超えるようにすると、やっぱりエラーになる。

どうやら、内部での計算は numeric 型の38桁の計算(これはint型よりもずっと大きい)でなされているようだということが分かる。

numeric( 8,3 ) で制限して10万回足したらどうなる?

numeric ってのは、有効桁数と小数点以下の桁数で指定するので、numeric( 8,3 ) は、有効桁数が8桁で小数点以下が3桁になる。99999.999 までが有効になる。

これで、1.999 のようなランダムな数を作って 10 万回足したらどうなるだろうか?と思ってやってみると、以下のようにふつうに sum の結果がでる。合計値は 535469.446 となるので、 numeric( 8,3 ) の範囲を超えているが、特に問題ないらしい。カラムの精度と計算結果の精度とは違うことがわかる。

image

たぶん、sum の計算結果は numeric の 38桁に拡張されているはずだ。

フルで 38 桁の精度を持たせた場合はどうなるのか?

では、カラムの設定を numeric( 38, 28 ) のようにして、有効桁数38桁、小数点以下28桁が有効にするとどうなるだろうか?

データとしては、1999999999.0000000000000000000000000001 を 20個ほど入れておいて、加算するとオーバーフローするように仕掛けておく。

insert into TT ( v, vv ) values ( 1, 1999999999.0000000000000000000000000001 );

こんな風にデータを入れておく。

image

そして sum する。

image

結果は予想通り、numeric の 38桁の精度を超えてしまうので計算できない。

じゃあ平均(AVG)の計算は?

平均を計算してみよう。同じ値しか入れていないので、平均は 1999999999.0000000000000000000000000001 で明白なんだけど、SQL Server の AVG 関数を使うと、以下のようにオーバーフローになる。

image

これ、前回の記事にも書いたが平均を計算する対象の最大値/最小値を適当に取ってきて、類推できる平均な値を使えば、オーバーフローにならずに計算することができる(当然、幅によってできないこともある)。だから、SQL Server の AVG 関数は、(おそらく)内部で SUM を使っていて個数で割ることをしているのだろう。だから、計算途中でオーバーフローしてしまうのだ。

「ドメイン知識」大切ですね、にもつながる。

カテゴリー: 雑談 | SQL Server を SUM/AVG でオーバーフローさせてみよう はコメントを受け付けていません

0.01を10000回足したら100.00にならない話

元ネタの引用リツイートが3件ほど立て続けてに回ってきたのと、「俺たちはテストコードにいつまで ε を書かないといけないのだろうか?」というツイートがあったので、それを絡めて。

最初に TDD の話だけ書いておこう(一連のツイートには書かなかったので)。

xUnit のテストコードに誤差範囲を指定する

自動単体テストでは、Assert.AreEqual( 期待値, 実測値 ) な形で、数値や文字列を比較するけど、これ実数(double)の場合は、Assert.AreEqual( 期待値, 実測値, 誤差 ) という形で誤差の許容範囲を指定しないといけない。これ、文字列や整数値に場合にはピッタリ一致するのだけど、実数の場合はそうはいかない。科学計算のユニットテストを書くとわかるが、計算順序や計算の精度などを上げると、微妙に値がずれる。そのたびにいちいち期待値を変えていられない、というか期待値を変えてしまってはテストコードにならないので、許容範囲を指定する。大抵の場合は有効桁数を書くことになるので、

Assert.AreEqual( 100.00, 実測値, 0.01)

な形で有効桁数を示すことが多い。この場合は、99.99001(99.99より大きい) から 100.00999…(100.01より小さい) を 100.00 と扱うことができる。不等号を使えば、

99.99 < 実測値 < 100.01

の範囲となる。両端を含まない「開区間」になるんだが、まあ細かいことはどうでもいい(本当は重要だけど)。こんな感じで真の値から幅のある値を取る。

コンピュータで扱う実数とは何か?

知識として整数値(int型)と実数値(doubleやfloat)を分けてコーディングをしないといけない、というのは知っている人が多いと思うのだけど、じゃあ double や float を使う場合、コンピュータ上でどう扱っているのか?というのを知っている人は…理/工学系には「教養」だよ!という話だったりする。

倍精度浮動小数点数 – Wikipedia https://ja.wikipedia.org/wiki/%E5%80%8D%E7%B2%BE%E5%BA%A6%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0

IEEE 754 Double Floating Point Format.svg

倍精度の「倍」の意味は、単精度の倍だから。なので、単精度が32ビット、倍精度が64ビットになる。8の倍数になっているのはコンピュータで扱いやすいからで、ここに当然ながら誤差が存在する。ざっと覚えておくとよいのは倍精度で8桁位まで有効に働く。10進数の有効数字としては、12桁位まであるんだけど、科学計算したりすると桁落ちが発生するので、経験上8桁までは有効になる。

元ネタが30年前の本を参照しているので、1990年代ぐらい。ちょうど情報科学学科ができ始めた頃で、C 言語が徐々に広まっていたころ。科学計算だと Fortran が主流でまだ一般的なプログラムとしては BASIC が主流だった頃の話だ。90年代としてひとくくりにしてしまうとかなり面倒なんだけど(インターネット接続も含まれるから)、Excel VBA が広まり始めた時期でもある。

メモリとバス幅の制約

ちょうど1990年代は、コンピュータ言語がいろいろできた時期でもあるので、このあたりの背景を説明すると、

  • 浮動小数点付きの計算は「高価」で、専用のコプロセッサで動かしていた
  • 高価だったから、普通の CPU はライブラリで浮動小数点計算をしていた。ので遅かった。
  • まだ、32ビットマシンが出たばかりで、レジスタやバス幅は 32 ビットだった
  • バス幅の関係上、単精度(32ビット)は早かったが、倍精度(64ビット)は遅かった。
  • ストレージ(フロッピー、HDD)が小さかったので、64ビットを保存するのは勿体なかった

という関係上、実数の計算は主に単精度(float)が使われていた。科学計算を専門に扱う Fortranはデフォルトが倍精度…と思っていたけど real(単精度)ですね。Fortran 入門: 定数と変数 http://www.nag-j.co.jp/fortran/FI_4.html を参照。自分の場合、炉心計算で倍精度にしていたらしい。

BASIC は 90年代よりも前からあるので、実数の場合「単精度」が使われている。なので、表現上は8桁位でるのだが、計算を繰り返しているうちに有効桁数は4桁位になってしまう。

これが1万回くりかえすと…の話になる。

ちなみに、Excel 2013 の VBA を使って計算をすると以下の計算は、100.000000000014 になる。デフォルトで倍精度(double)になっている。

image

実運用上問題はないけど、これを 100.00 と比較すると「偽」になる。当然だ。実数の比較するときには、先の xUnit の例で述べた通り有効範囲を指定しないとダメだからだ。

加算の繰り返しで誤差が累積する

先の計算で、手っ取り早く誤差をうまないためには「実数を整数の範囲に直す」という計算方法が使われる。科学計算の場合は有効桁数で済ませられるのだが、金融関係ではそれではダメだったりする。お金の問題は最後の1円まで有効になる。

お金の計算はいくつか特殊で、

  • 消費税で円以下は切り捨て
  • 為替計算では銭単位まで計算(だったとはず)
  • 利息では銭まで計算
  • 利子は円で切り捨て
  • 四捨五入ではなく銀行まるめを使う

という特有なルールがある。なので、0.01ドル(1セント)のような小数点以下2桁までは重要だったりする。これが1万回加算されたとき(積み立てたとき?)に100.00ドルぴったりにならないと困るわけだ。

最近のシステムでは、decimal や money などのお金を扱うための型が用意されているが(中身も10進数で計算する)、90年代にはそういうものがなかったので、COBOL のように10進数計算ができるプログラム言語を使うか、自前で作るしかなかった。

ただし、0.01 単位というのが分かっていれば、手っ取り早く 100 倍して整数に直し、結果を100で割ればよい。

image

な感じに書き換えると、答えは「100.00」ぴったりになる。

浮動小数点をコンピュータで扱うと有効桁以下のところの誤差が当然でてくる。これを100回加算すれば2桁誤差が増えるし、1万回加算すれば4桁誤差が上がってくる。なので、実数をあつかうときに注意しないといけないのは、

  • 有効桁数
  • 小数点以下の固定の桁数

とは違うということだ。ソフトウェア工学の教科書の最初に出てくる問題なので「教養」の範疇だと思うし、統計をコンピュータ処理するときの常識=「知っておくべき知識」だと思うのだが、どうだろうか?

有効桁数はよく聞かれるので、有効桁数4桁といえば「1234000」な形や「1.234」な形で示される数字の部分だ。小数点以下でゼロをつけるて明示的に「1.000」として有効桁数を表す場合も多い。

これと混同しやすいのは、小数点以下の固定の桁数で「100.234」みたいな形のもの。有効桁数は6桁と考ええても良いけど、この場合は小数点以下の桁数のほうが重要で、固定小数点で小数点以下3桁有効、という言い方をする。浮動小数点は指数表示で表してもいいけど(有効桁数のほうが重要なので)、固定小数点の場合は小数点以下の桁数が重要になるので指数表現するとまずい場合が多い。

100.234 の表現の場合、100 の回りに値が散らばっていて 101.000 とか、98.123 とかの値があるということだ。この場合、測定値から 100.00 を引いてやれば、桁あふれの危険がすくなくなる。0.234 とか 1.000 とか –2.123 とかにならうからだ。これだと有効桁数もわかりやすい。

統計を計算するときに、平均とか分散を計算するのだが、単純に測定値を加算していくつ桁あふれをしてしまう。1万位なら大したことがないとおもうだろけど、100万件のデータの平均を出すときに単純に加算してしまうと桁あふれ(オーバーフロー)を起こしてしまうのは明白だ。だから、先のように 100 の周辺に値が集まっている(身長などはそれ)場合には、だいたい平均っぽい値を先に引いてしまう。これを「ドメイン知識」という(というのを心理統計法で最近知った)。いわゆる、統計をとったときの特性(ドメイン知識)がないと単純に加算してオーバフローを起こすか誤差を累積させてしまうが、適度な知識があれば、誤差を抑えて精度のよい計算ができる。

物理的な有効桁数は意外と少ない

キログラム原器から プランク定数 をもとに計算するように変わったわけだが、プランク定数 6.626 070 040(81)×10-34 J s の桁数がそのまま計測値に適用できるわけではない。というか、たくさんの桁数があっても意味はない(測定誤差があるので)。

なので、測定誤差も含めて材料力学では2,3桁の有効数字で十分だし(安全係数が、1.2 のように実質小数点以下1桁ぐらいなのから推測できる)、天文学に至っては log で計算してしまうので1桁あれば十分だったりする。そもそも、物理の世界では、値が倍ぐらい違ってもびくともしないので、有効桁数は0.5桁という、有効桁数が小数点以下という感覚もある。電子工学でも抵抗の値は10%位の誤差を許容する(誤差自体が書いてある)し、温度や湿度、電流によって特性グラフがデータシートとしてあるので、ぴったりとした値が決められる訳ではない。むしろ、誤差なり特性なりを含む許容範囲が必要となる。

こんな感じに、値が分布していて許容できる上限下限を決める。部品や測定値は真ん中に集中する(正規分布など)グラフになる。

image

なので、計算誤差が累積したり測定誤差があったりするのを前提として、この範囲内で「正しい計算」を使用とするのが科学計算である。だから、実数を扱う場合と整数を扱う場合はちょっと違うことを意識しないといけない。

乗除算でも誤差を減らすためのテクニックがあって、

  • A / B * C
  • A * C / B

この2つの答えは同じように見えるが、後者の「A * C / B」のほうが精度がよく計算できる。前者のように割り算を先にやってしまうと、割り切れない場合があるので誤差がでる。それを次の C を掛けることで増幅させているのだ。だから、数値のオーバーフローに注意してという前提はあるが、A * C で誤算なく乗算をした後に、B で割る(割ったとき割り切れなくてよい)という式変形をする。

数式上(無限に有効桁数があると考えらる)では同じだが、コンピュータ上の有限な有効桁数があると制限がでてくるという話である。

p値(有意確率)と誤差伝播

一気に統計値の話になって、AI な話に振っていきたいのだが、この誤差の部分と分布をうまく扱っていかないと、AI で言われるところの「検定」がうまく働かない。学会や論文的には p 値が重要になってくるのだが、測定誤差とか標本分布の確率の話がないのに、一気に p 値にはいってしまうのはどうかと思う…という話を書きたいところなのだけど、まだそこまで勉強が至っていないので今回は割愛。誤差伝播に関しては、隠れマルコフモデル(HMM)で画像解析をやろうとした時期があって、ベイズは少し慣れているんだが。

プロジェクト管理手法の優位性検定とか不具合検出の標本分布とかは計算しておきたいところ。それはまた後日。

カテゴリー: 雑談 | 0.01を10000回足したら100.00にならない話 はコメントを受け付けていません