01. 기초 통계 값 확인하기 [Math.NET, MS Chart]

안녕하세요. 언제나 휴일입니다.

C#으로 통계 및 확률 등의 분석 프로그램을 작성하기 위해서는 Math.NET을 설치하면 효과적입니다.

이전 게시글에서 Math.NET을 설치하였습니다.

이번에는 기초 통계 값을 확인하는 응용을 만들어 봅시다.

1. 프로젝트 소개

먼저 실행 모습을 보면서 얘기할게요.

기초 통계 값 확인 응용 실행 화면
기초 통계 값 확인 응용 실행 화면

사용자가 최솟값과 최댓값 및 샘플 개수를 입력하여 샘플 생성 버튼을 클릭합니다.

프로그램에서는 구간 [최솟값,최댓값] 사이의 랜덤한 값을 요청한 개수만큼 생성하여 왼쪽 리스트 박스에 보여줍니다.

가운데 리스트 박스에는 생성한 샘플의 평균, 중간값, 최솟값, 최댓값, 1/4분위값, 3/4분위값, 분산, 표준편차, 모분산, 모표준편차를 보여줍니다.

오른쪽 리스트 박스에는 구간(구간 시작, 구간 끝]에 해당하는 샘플 개수를 보여줍니다.

아래에는 히스토그램으로 시각화합니다.(MS Chart 이용)

2. 컨트롤 배치

프로젝트는 Windows Forms(.NET Framework)로 만들었어요.

Math.NET 라이브러리를 이용하기 위해서는 MathNet.Numerics와 System.ValuTuple 어셈블리를 참조해야 합니다.

설치한 곳에서 적절한 버전의 라이브러리를 참조 추가하세요.

제 환경에서는 net461 폴더에 있는 어셈블리를 추가하였습니다.

MathNet.Numerics 어셈블리 추가
MathNet.Numerics 어셈블리 추가

System.ValueTuple 어셈블리도 같은 버전으로 어셈블리를 추가하세요.

System.ValueTuple 참조 추가
System.ValueTuple 참조 추가

다음 그림처럼 자식 컨트롤을 배치하고 이름 및 속성을 설정하세요.

자식 컨트롤 배치
자식 컨트롤 배치

3. 구현

3.1 btn_make_sample 클릭 이벤트 핸들러 구현

btn_make_sample의 클릭 이벤트 핸들러를 추가하세요. (이벤트 핸들러를 추가해야 합니다. 코드만 똑같게 작성한다고 동작하지 않습니다.)

        private void btn_make_sample_Click(object sender, EventArgs e)
        {     

샘플을 생성할 랜덤 개체를 생성할게요.

            Random rand = new Random();

사용자가 입력한 최솟값, 최댓값, 샘플 개수를 가져옵니다. (예외 처리는 하지 않겠습니다.)

            int min = int.Parse(tbox_gmin.Text);
            int max = int.Parse(tbox_gmax.Text);
            int n = int.Parse(tbox_sample_size.Text);  

랜덤 개체를 이용하여 min~max 사이의 랜덤한 값을 생성합니다.

주의할 점은 랜덤 개체에서 max값 미만의 값까지 발생하므로 max+1로 전달합니다.

            double[] samples = new double[n];
            for(int i = 0;i<n;i++)
            {
                samples[i] = rand.Next(min, max+1);
            }

lbox_sample의 DataSource로 samples을 설정하고 분석 및 차트 설정 메서드를 호출할게요.

(분석 및 챠트 설정 메서드는 구현할 것입니다.)

            lbox_sample.DataSource = samples;
            Analyze(samples);
            SetChart(samples,min,max);

3.2 분석 메서드 구현

분석 메서드는 샘플을 입력 인자로 받습니다.

        private void Analyze(double[] samples)
        {

lbox_summary의 항목을 지웁니다.

            lbox_summary.Items.Clear();

System.ValuTuple 어셈블리를 추가하였기에 기초 통계 값을 쉽게 확인할 수 있습니다.

입력인자로 받은 samples(double 원소 배열)에 멤서 메서드로 Mean, Median, Min, Max, Quantile, Variance, StandardDeviation, PopulationVariance, PopulationStandardDeviation를 제공하고 있습니다.

Population이 붙어 있는 것은 모분산과 모표준편차를 구하는 메서드입니다. 샘플 데이터가 모집단일 때 이를 사용합니다.

표본 데이터일 때는 Population이 붙어 있지 않은 것을 사용합니다.

            lbox_summary.Items.Add(string.Format("평균:{0}", samples.Mean()));
            lbox_summary.Items.Add(string.Format("중간값:{0}", samples.Median()));
            lbox_summary.Items.Add(string.Format("최솟값:{0}", samples.Min()));
            lbox_summary.Items.Add(string.Format("최댓값:{0}", samples.Max()));
            lbox_summary.Items.Add(string.Format("1/4분위값:{0}", samples.Quantile(0.25)));
            lbox_summary.Items.Add(string.Format("3/4분위값:{0}", samples.Quantile(0.75)));
            lbox_summary.Items.Add(string.Format("분산:{0}", samples.Variance()));
            lbox_summary.Items.Add(string.Format("표준편차:{0}", samples.StandardDeviation()));
            lbox_summary.Items.Add(string.Format("모분산:{0}", samples.PopulationVariance()));
            lbox_summary.Items.Add(string.Format("모표준편차:{0}", samples.PopulationStandardDeviation()));

3.3 챠트 설정 메서드 구현

챠트 설정 메서드는 샘플과 최솟값, 최댓값을 전달합니다.

        private void SetChart(double[] samples, int min, int max)
        {            

Math.NET에서는 Histogram(히스토그램) 클래스를 제공합니다.

히스토그램 클래스 중에 샘플, 최솟값, 최댓값을 입력 인자로 받는 생성자를 호출할게요.

            Histogram his = new Histogram(samples, max-min+1, min-1, max);

챠트의 x축에 Tick 간격을 max-min/5로 정할게요.

            int width = (int)(max - min) / 5;

chart를 배치하면 하나의 Series는 있는 상태입니다.

첫 번째 Series에 데이터를 보관하는 Points 컬렉션을 비워줍니다.

            chart.Series[0].Points.Clear(); 

히스토그램은 막대 그래프와 비슷하지만 막대 사이에 간격을 두지 않습니다. 이를 위해 PointWidth를 1로 설정할게요. (디폴트는 0.8입니다.)

            chart.Series[0].SetCustomProperty("PointWidth", "1");

챠트에 X 영역은 최솟값 – width에서 최댓값 +width까지 표시하고 tick 인터벌을 width로 설정합니다.

            chart.ChartAreas[0].AxisX.Minimum = min- width;
            chart.ChartAreas[0].AxisX.Maximum = max+ width;
            chart.ChartAreas[0].AxisX.Interval = width;

이제 챠트와 lbox_hist에 히스토그램의 버켓(단위 구간과 개수) 정보를 추가할 것입니다.

lbox_hist의 항목을 먼저 비워줄게요.

            lbox_hist.Items.Clear();

버켓 정보를 lbox_hist와 챠트에 추가합니다.

버켓에는 구간의 시작(LowerBound), 구간의 끝(UpperBound) 및 개수(Count) 정보 등을 확인할 수 있습니다.

            for (int i = 0; i < his.BucketCount; i++)
            {
                double tick = (his[i].LowerBound + his[i].UpperBound) / 2;
                chart.Series[0].Points.AddXY(tick, his[i].Count);
                lbox_hist.Items.Add(string.Format("({0:000},{1:000}]:{2}", 
                    his[i].LowerBound, his[i].UpperBound, his[i].Count));
                
            }

4. Form1.cs 소스 코드

Form1.cs 소스 코드는 다음과 같습니다.

using MathNet.Numerics.Statistics;
using System;
using System.Linq;
using System.Windows.Forms;

namespace _002.기초_통계
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void btn_make_sample_Click(object sender, EventArgs e)
        {            
            Random rand = new Random();
            int min = int.Parse(tbox_gmin.Text);
            int max = int.Parse(tbox_gmax.Text);
            int n = int.Parse(tbox_sample_size.Text);            
            double[] samples = new double[n];
            for(int i = 0;i<n;i++)
            {
                samples[i] = rand.Next(min, max+1);
            }
            lbox_sample.DataSource = samples;
            Analyze(samples);
            SetChart(samples,min,max);
        }

        private void SetChart(double[] samples, int min, int max)
        {            
            
            Histogram his = new Histogram(samples, max-min+1, min-1, max);
            
            int width = (int)(max - min) / 5;
            chart.Series[0].Points.Clear(); 
            chart.Series[0].SetCustomProperty("PointWidth", "1");
            chart.ChartAreas[0].AxisX.Minimum = min- width;
            chart.ChartAreas[0].AxisX.Maximum = max+ width;
            chart.ChartAreas[0].AxisX.Interval = width;
            lbox_hist.Items.Clear();
            for (int i = 0; i < his.BucketCount; i++)
            {
                double tick = (his[i].LowerBound + his[i].UpperBound) / 2;
                chart.Series[0].Points.AddXY(tick, his[i].Count);
                lbox_hist.Items.Add(string.Format("({0:000},{1:000}]:{2}", 
                    his[i].LowerBound, his[i].UpperBound, his[i].Count));
                
            }
        }

        private void Analyze(double[] samples)
        {
            lbox_summary.Items.Clear();
            lbox_summary.Items.Add(string.Format("평균:{0}", samples.Mean()));
            lbox_summary.Items.Add(string.Format("중간값:{0}", samples.Median()));
            lbox_summary.Items.Add(string.Format("최솟값:{0}", samples.Min()));
            lbox_summary.Items.Add(string.Format("최댓값:{0}", samples.Max()));
            lbox_summary.Items.Add(string.Format("1/4분위값:{0}", samples.Quantile(0.25)));
            lbox_summary.Items.Add(string.Format("3/4분위값:{0}", samples.Quantile(0.75)));
            lbox_summary.Items.Add(string.Format("분산:{0}", samples.Variance()));
            lbox_summary.Items.Add(string.Format("표준편차:{0}", samples.StandardDeviation()));
            lbox_summary.Items.Add(string.Format("모분산:{0}", samples.PopulationVariance()));
            lbox_summary.Items.Add(string.Format("모표준편차:{0}", samples.PopulationStandardDeviation()));
        }
    }
}

5. More

Random 클래스가 발생하는 값은 균등 분포에 가깝습니다.

샘플 개수가 커질 수록 분포가 균일함을 알 수 있습니다.

샘플 40개
샘플 40개
샘플 1000개
샘플 1000개
샘플 50000개
샘플 50000개

만약 사람의 키를 샘플 데이터로 만든다고 한다면 균일 분포의 샘플을 만드는 것은 적절치 않습니다.

앞으로 정규 분포, T 분포, 균일 분포, 이항 분포, 지수 분포, 포아송 분포에 관한 프로젝트를 하나 하나 만들어 볼게요.

그리고 다양한 검정 방법에 대해서도 다룰 거예요.