私はelmと統計学の初心者でかつ初学者です。何か間違っているところがあったらコメントお願いします。
ソースコード:https://github.com/saburooo/simple-stat
成果物:https://mysimple-stat.web.app
NEW : SVG初心者です。
データについて
統計に使うデータは数字の羅列でかつすぐに計算に使う、私はそう思っていましたが違うみたいです。
データを実際に収集してまず行うことは収集されたデータ達を読むことです。
そのために何をすればいいのか、それはグラフの作成です。elmにはSVGを操作するライブラリがたくさんあるのでその中からTypedSvgというライブラリを使ってグラフを作成していきたいと思います。
ヒストグラムの原型を作る。
histgramBar: Float -> Float -> Svg.Svg msg
histgramBar x h =
Svg.rect [ InEm.width 1, height (Types.percent 100), InEm.x x, InEm.height h
, TypedSvg.Attributes.style "transform: scale(1, -1)"
, fill (Types.Paint Color.blue)
, strokeWidth (Types.px 2)
, stroke ( Types.Paint Color.darkBlue )
] []
これでヒストグラムを表現できると考えたわけですがこのまま適用した場合、画面内に四角形が収まらなくなる危険性があります。
そこで以下の概念を使ってヒストグラムを作成していこうと思います。
度数分布表
計算を行う前にとりあえず作っておくものでデータは大体どんな感じなのかを見極めるために作成されます。
度数とは簡単に言うと「何何以上ホゲホゲ未満」の範囲に入った数値の数のことです。
階級
階級とは手に入れたデータをグループ分けするために作るものです。
階級値
階級値とはちょうどその階級を代表するものとされており、大体においてその階級の範囲の値の真ん中くらいの値になります。
テーブルにするとこんな感じです。
階級 | 階級値 | 度数 |
---|---|---|
hoge以上fuga未満 | hogeとfugaの狭間 | hogefuga |
fuga以上piyo未満 | fugaとpiyoの間 | fugapiyo |
階級幅
階級幅とはグラフを作る際に幅をどうやって決めるか計算する方法で、目安の決め方には色々なやり方がありますが、今回は受け取ったデータの中で最も大きいものと小さい物を引いてデータの数の分だけ割っていく方式で求めていきます。
階級の数を決めるために
グラフを作っていく上で必要になっていくのは階級の数、つまり区切りをどうするかなのですがそれを求める式としてスタージェスの公式があります。(あくまで目安です。)以上を踏まえてelmで実装していくと。
starJes : List Float -> Int
starJes argList =
round <| 1 + logBase 2 (toFloat (List.length argList))
これを使っていい感じに階級が分かれたグラフを作っていこうと思います。
先程のスタージェスの公式を使って階級幅を決める。
まずは境界の値を求めてみたいと思います。
bundary : Float
bundary = ( Maybe.withDefault 0 ( Maybe.map2 (-) ( List.maximum floatList ) ( List.minimum floatList ) ) ) / toFloat star
この辺は私のオリジナル実装ですがこのようにしました。
classInterval : List (Float, Float)
classInterval = ( List.map2 (Tuple.pair) ( List.map (\s -> toFloat s * bundary) ( List.range 0 star ) ) ( List.map (\s -> toFloat s * bundary) ( List.range 1 ( star + 1 ) ) ) )
これらを変数として組み込んだ関数としてappendClass
を実装しようと思います。
appendClass: List Float -> Dict ( Float, Float ) Float
appendClass floatList =
let
star = Utility.starJes floatList
bundary = ( Maybe.withDefault 0 ( List.maximum floatList ) - ( Maybe.withDefault 0 ( List.minimum floatList ) ) ) / toFloat star
classInterval = ( List.map2 (Tuple.pair) ( List.map (\s -> toFloat s * bundary |> roundNum 2) ( List.range 0 ( star + 1 ) ) ) ( List.map (\s -> toFloat s * bundary |> roundNum 2) ( List.range 1 ( star + 2 ) ) ) )
frequencyList = ( List.map (\cls -> frequency cls floatList) classInterval )
in
Dict.fromList frequencyList
これをもとにList Floatを受け取ってヒストグラムなSVGを返す関数を実装してみたいと思います。
listHistgram:List Float -> Svg.Svg msg
listHistgram floatList =
let
indexed = appendClass floatList
dictRange = Dict.size indexed |> List.range 0 |> List.map (\x -> toFloat x)
inserted = List.map2 (Tuple.pair) dictRange (Dict.values indexed )
in
Svg.svg [ viewBox 0 0 800 250 ]
[ backColor Color.lightBlue
, TypedSvg.g [ transform [ Types.Translate 0 198 ] ] (List.map (\h -> histgramBar ( Tuple.first h ) ( Tuple.second h )) inserted)
, TypedSvg.text_ [ transform [ Types.Translate 0 230 ], fontSize 0.7, fill (Types.Paint Color.white) ] [ text ( listTupleStr (Dict.keys indexed) ) ]
]
結構いい感じになりましたがまだまだ見た目が気になるのと想定した形にならなくなる不具合があるので試行錯誤を重ねていく所存です。
更に追記:円グラフが実装できました。
参考になったのはこちらのサイトです。
色々手を加えたあとに書き足したので元サイトとは違ったものになっていますがソースコードはこんな感じになります。
listCircle: List Float -> Svg.Svg msg
listCircle floatList =
let
indexed = appendClass floatList
dictRange = Dict.size indexed |> List.range 0 |> List.map (\x -> toFloat x)
inserted = List.map2 (Tuple.pair) dictRange (Dict.values indexed )
total = List.sum ( List.map (\c -> Tuple.second c) inserted )
in
Svg.svg [ viewBox 0 0 63.6619772368 63.6619772368, width (Types.px 300) ]
[ backColor Color.lightBlue
, TypedSvg.g [ transform [ Types.Translate -0.1 63.5 ] ]
( List.map (\c -> circleMap c total (offsetPickUp c inserted)) inserted )
]
この関数で List Float を受け取ったら
offsetPickUp: (Float, Float) -> List (Float, Float) -> Float
offsetPickUp tuple tupleList =
let
total = List.sum (List.map (\c -> Tuple.second c) tupleList)
pickUp = List.filter (\c -> Tuple.first c <= Tuple.first tuple) tupleList
in
List.foldl (+) 0 (List.map (\c -> Tuple.second c) pickUp) * 100 / total |> roundNum 2
こちらで回転の度合いを求めつつ
circleMap: (Float, Float) -> Float -> Float -> Svg.Svg msg
circleMap tuple total offset =
let
first = Tuple.first tuple
counts = Tuple.second tuple
parcentage = 100.0 * counts / total
in
TypedSvg.circle
[ cx (Types.px 31.8309886184 )
, cy (Types.px -31.8309886184 )
, r (Types.px 15.9154943092 )
, fill Types.PaintNone
, stroke ( Types.Paint ( Color.rgb ( first / 10 + 0.2 ) ( first / 10 + 0.2 ) ( first / 10 + 0.5 ) ) )
, strokeWidth (Types.px 31.8309886184 )
, strokeDashoffset ( 25 - offset + parcentage |> String.fromFloat )
, strokeDasharray (( parcentage |> String.fromFloat ) ++ "," ++ ( 100.0 - parcentage |> String.fromFloat ))
]
[ ]
こうやって円グラフを実装しました。見ての通りviewBoxに対して円が大きすぎるのでできれば小さくしていきたいです。
おまけ
このSVGテキストの方ですが以下のやり方で実装しました。
listTupleStr: List (Float, Float) -> String
listTupleStr listTuple =
let
first = List.unzip listTuple |> Tuple.first |> List.map (\f -> String.fromFloat f ++ "から")
second = List.unzip listTuple |> Tuple.second |> List.map (\f -> String.fromFloat f ++ "まで, ")
in
List.map2 (++) first second |> String.concat