Pull to refresh

«Письмо турецкому султану» или линейная регрессия на C# с помощью Accord.NET для анализа открытых данных Москвы

Reading time 13 min
Views 12K
Когда речь идет об освоении самых основ машинного обучения, чаще всего предлагается изучить соответствующие инструменты на Python или R. Мы не будем обсуждать их плюсы и минусы, а просто зададимся вопросом, что делать если вы знакомы только с экосистемой .NET, но при этом вам очень любопытно окунутся в мир науки о данных? Ответ прост, не отчаиваться и посмотреть в сторону F#, а если вы также, как и я из .NET знаете только азы C#, то попробовать изучить Accord.NET Framework.

Мы с вами уже разбирали его применение для решения задачи классификации, а в этот раз попробуем рассмотреть простейший инструментарий для линейной регрессии. Для этого мы воспользуемся открытыми данными по анализу обращений граждан взятыми с официального сайта Мэра Москвы.

Несмотря на то, что в заголовке статьи указан C#, мы попробуем собрать код и на VB.NET.

Мне осталось только пригласить вас под кат!




Чтобы избежать подобных комментариев, сразу в самом начале скажу, что я не имею никакого отношения к правительству Москвы, управам, префектурам и т.п. поэтому нет смысла мне жаловаться на их работу. Я просто случайно нашел эти данные, вручную забил их в табличку и выложил для вас на GitHub.

Ну и еще если кому любопытно, то эта статья продолжает мини-цикл, посвященный тому как я учился Data Science с нуля (и так толком не выучился), если кому интересно, то я спрятал ссылки на остальные статьи под спойлер.


Честно признаюсь я не программист, к тому же Accord.NET я изучил очень поверхностно. К сожалению по нему не так много литературы, да и учебных on-line курсов как-то сходу не нашлось, так что во многом остается только сайт разработчиков, а он не так информативен, как хотелось бы.

Поэтому с предложенным выше набором данных основные манипуляции я проводил в прошлой статье цикла (там же и более подробное описание набора данных). А в данной статье, мы со скрипом попробуем прочитать данные, обучить модель и построить какой-нибудь график.

Содержание:

Часть I: введение и немного о данных
Часть II: пишем код на C#
Часть III: пишем код на VB и заключение

Прежде чем начать писать код — два слова о данных.
Это открытые данные по анализу обращений граждан поступивший в адрес тех или иных органов исполнительной власти г. Москвы. Надо сказать, что статистика скудная, пока всего 22 месяца.

По факту могло бы быть – 23 месяца, но в ноябре разработчики предоставили неполный комплект данных, и я его не включил.
Данные представлены в формате csv. Столбцы данных означают следующее:

num – Индекс записи
year – год записи
month – месяц записи
total_appeals – общее количество обращений за месяц
appeals_to_mayor – общее количество обращений в адрес Мэра
res_positive- количество положительных решений
res_explained – количество обращений на которые дали разъяснения
res_negative – количество обращений с отрицательным решением
El_form_to_mayor – количество обращений к Мэру в электронной форме
Pap_form_to_mayor - количество обращений к Мэру на бумажных носителях to_10K_total_VAO…to_10K_total_YUZAO – количество обращений на 10000 населения в различных округах Москвы
to_10K_mayor_VAO… to_10K_mayor_YUZAO– количество обращений в адрес Мэра и правительства Москвы на 10000 населения в различных округах города

Я не нашел способа автоматизировать процесс сбора данных и их пришлось в итоге собирать руками, так что я мог где-то слегка ошибиться, ну и саму по себе достоверность данных оставлю на совести авторов.

Осталось рассказать буквально пару слов о самом фреймворке и можно переходить к коду.
Accord.NET – это проект с открытым исходным кодом, который тем не менее в большинстве случаев может использоваться для коммерческой разработки под лицензией LGPL. Похоже, что фреймворк имеет весь базовый инструментарий необходимый для анализа данных и машинного обучения, от проверки статистических гипотез до нейронных сетей.

Теперь можно с чистой совестью перейти к коду.
Решение с проектом на C# и VB.NET я выложил для вас на GitHub, вы можете просто его скачать и попробовать собрать (по идее должно запуститься). Если вы хотите сами создать проект с нуля, то для аналогичного функционала необходимо сделать следующее:

  1. Создать новый проект (я создал консольный проект с Net Framework 4.5).
  2. С помощью менеджера пакетов (NuGet) установить Accord.Controls версии 3.8 (он потянет все остальные нужные нам пакеты), а также Accord.IO для работы с таблицами. Также для отрисовки графика понадобится включить стандартную библиотеку Windows.Forms. Вот собственно и всё можно писать код.

Полный код на C# размещу под спойлером.

Полный код для C#
using System;
using System.Linq;
using Accord.Statistics.Models.Regression.Linear;
using Accord.IO;
using Accord.Math;
using System.Data;
using System.Collections.Generic;
using Accord.Controls;
using Accord.Math.Optimization.Losses;

namespace cs_msc_mayor
{
    class Program
    {
       
        static void Main(string[] args)
        {
            //for separating the training and test samples
            int traintPos = 18;
            int testPos = 22;
            int allData = testPos + (testPos - traintPos);


            //for correct reading symbol of float point in csv
            System.Globalization.CultureInfo customCulture = (System.Globalization.CultureInfo)System.Threading.Thread.CurrentThread.CurrentCulture.Clone();
            customCulture.NumberFormat.NumberDecimalSeparator = ".";
            System.Threading.Thread.CurrentThread.CurrentCulture = customCulture;


            //read data
            string CsvFilePath = @"msc_appel_data.csv";
            DataTable mscTable = new CsvReader(CsvFilePath, true).ToTable();

            //for encoding the string values of months into numerical values
            Dictionary<string, double> monthNames = new Dictionary<string, double>
            {
                ["January"] = 1,
                ["February"] = 2,
                ["March"] = 3,
                ["April"] = 4,
                ["May"] = 5,
                ["June"] = 6,
                ["July"] = 7,
                ["August"] = 8,
                ["September"] = 9,
                ["October"] = 10,
                ["November"] = 11,
                ["December"] = 12

            };


            string[] months = mscTable.Columns["month"].ToArray<String>();
            double[] dMonths= new double[months.Length];

            for (int i=0; i< months.Length; i++)
            {
                dMonths[i] = monthNames[months[i]];
                //Console.WriteLine(dMonths[i]);
            }

            //select the target column
            double[] OutResPositive = mscTable.Columns["res_positive"].ToArray();

            // separation of the test and train target sample
            double[] OutResPositiveTrain = OutResPositive.Get(0, traintPos);
            double[] OutResPositiveTest = OutResPositive.Get(traintPos, testPos);

            //deleting unneeded columns
            mscTable.Columns.Remove("total_appeals");
            mscTable.Columns.Remove("month");
            mscTable.Columns.Remove("res_positive");
            mscTable.Columns.Remove("year");


            //add coded in a double column month into Table

            //create new column
            DataColumn newCol = new DataColumn("dMonth", typeof(double));
            newCol.AllowDBNull = true;
            // add new column
            mscTable.Columns.Add(newCol);

            //fill new column
            int counter = 0;
            foreach (DataRow row in mscTable.Rows)
            {
                row["dMonth"] = dMonths[counter];
                counter++;
            }


            //receiving input data from a table
            double[][] inputs = mscTable.ToArray();

            

            //separation of the test and train sample
            double[][] inputsTrain= inputs.Get(0, traintPos);
            double[][] inputsTest = inputs.Get(traintPos, testPos);



            //simple linear regression model
            var ols = new OrdinaryLeastSquares()
            {
                UseIntercept = true
            };

            //linear regression model for several features
            MultipleLinearRegression regression = ols.Learn(inputsTrain, OutResPositiveTrain);

            //make a prediction
            double[] predicted = regression.Transform(inputsTest);

            //console output

            for (int i = 0; i < testPos - traintPos; i++)
            {
                Console.WriteLine("predicted: {0}   real: {1}", predicted[i], OutResPositiveTest[i]);
            }
            // And  print the squared error using the SquareLoss class:
            Console.WriteLine("error = {0}", new SquareLoss(OutResPositiveTest).Loss(predicted));

            // print the coefficient of determination
            double r2 = new RSquaredLoss(numberOfInputs: 29, expected: OutResPositiveTest).Loss(predicted); 
            Console.WriteLine("R^2 = {0}", r2);

            // alternative print the coefficient of determination
            double ur2 = regression.CoefficientOfDetermination(inputs, OutResPositiveTest, adjust: true);
            Console.WriteLine("alternative version of R2 = {0}", r2);

            Console.WriteLine("Press enter and close chart to exit");

            // for chart 

            int[] classes = new int[allData];
            double[] mountX = new double[allData];
            for (int i = 0; i < allData; i++)
            {
                if (i<testPos)
                {
                   // for csv data
                    mountX[i] = i+1;
                    classes[i] = 0; //csv data is class 0
                }
                else
                {
                    //for predicted
                    mountX[i] = i- (testPos - traintPos)+1;
                    classes[i] = 1; //predicted is class 1
                }

                
            }

            // make points of chart
            List<double> OutChart = new List<double>();
            OutChart.AddRange(OutResPositive);
            OutChart.AddRange(predicted);

           
            // plot chart
            ScatterplotBox.Show("res_positive from months", mountX, OutChart.ToArray(), classes).Hold();

            // for pause
            Console.ReadLine();
        }
    }
}


Во многом решение задачи линейной регрессии взято из примера с сайта разработчиков, там всё не очень сложно, но все же давайте разберем код по частям.

using System;
using System.Linq;
using Accord.Statistics.Models.Regression.Linear;
using Accord.IO;
using Accord.Math;
using System.Data;
using System.Collections.Generic;
using Accord.Controls;
using Accord.Math.Optimization.Losses;

Загружаем пространства имен сторонних библиотек.

namespace cs_msc_mayor
{
    class Program
    {
       
        static void Main(string[] args)
        {

Создаем пространство имен, класс, главный метод – всё тривиально.


            //for separating the training and test samples
            int traintPos = 18;
            int testPos = 22;
            int allData = testPos + (testPos - traintPos);

Определяем переменные, которые впоследствии нам пригодятся, чтобы разделить данные на контрольную и обучающие выборки.


            //for correct reading symbol of float point in csv
            System.Globalization.CultureInfo customCulture = (System.Globalization.CultureInfo)System.Threading.Thread.CurrentThread.CurrentCulture.Clone();
            customCulture.NumberFormat.NumberDecimalSeparator = ".";
            System.Threading.Thread.CurrentThread.CurrentCulture = customCulture;

Это нам пригодится, чтобы наш разделитель дробной части числа читался одинаково и в версии проекта на python и в версии на .NET (по крайней мере у меня).

            //read data
            string CsvFilePath = @"msc_appel_data.csv";
            DataTable mscTable = new CsvReader(CsvFilePath, true).ToTable();

Считываем данные из csv файла в формат таблицы данных.

            //for encoding the string values of months into numerical values
            Dictionary<string, double> monthNames = new Dictionary<string, double>
            {
                ["January"] = 1,
                ["February"] = 2,
                ["March"] = 3,
                ["April"] = 4,
                ["May"] = 5,
                ["June"] = 6,
                ["July"] = 7,
                ["August"] = 8,
                ["September"] = 9,
                ["October"] = 10,
                ["November"] = 11,
                ["December"] = 12

            };
            string[] months = mscTable.Columns["month"].ToArray<String>();
            double[] dMonths= new double[months.Length];

            for (int i=0; i< months.Length; i++)
            {
                dMonths[i] = monthNames[months[i]];
                //Console.WriteLine(dMonths[i]);
            }

Чтобы обрабатывать данные о том в какой месяц происходили обращения, необходимо перевести их в удобоваримый формат, в данном случае всё будем кодировать в тип double.
По аналогии с решением на Python мы вначале создадим словарь, а потом перекодируем данные в соответствии с ним с помощью цикла.

//select the target column
            double[] OutResPositive = mscTable.Columns["res_positive"].ToArray();

            // separation of the test and train target sample
            double[] OutResPositiveTrain = OutResPositive.Get(0, traintPos);
            double[] OutResPositiveTest = OutResPositive.Get(traintPos, testPos);

Выделим целевую функцию. Прогнозировать мы с вами будем число положительных решений на все обращения.
В первой строке мы вытаскиваем эти данные из таблицы преобразуя к типу double.
А затем в другие две переменные копируем позиции с 0 по 18 для учебной выборки и с 18 до 22 для контрольной выборки.

            //deleting unneeded columns
            mscTable.Columns.Remove("total_appeals");
            mscTable.Columns.Remove("month");
            mscTable.Columns.Remove("res_positive");
            mscTable.Columns.Remove("year");

Удаляем из таблицы ненужные столбцы: нашу целевую функцию, месяцы, годы, и общее число обращений, потому что оно включает в себя сведения о положительном результате рассмотрения.

//add coded in a double column month into Table

            //create new column
            DataColumn newCol = new DataColumn("dMonth", typeof(double));
            newCol.AllowDBNull = true;
            // add new column
            mscTable.Columns.Add(newCol);

            //fill new column
            int counter = 0;
            foreach (DataRow row in mscTable.Rows)
            {
                row["dMonth"] = dMonths[counter];
                counter++;
            }

А теперь, добавим столбец с перекодированным месяцами, для начала создаём новую колонку, добавим её к таблице, а потом в цикле заполним.

            //receiving input data from a table
            double[][] inputs = mscTable.ToArray();

            

            //separation of the test and train sample
            double[][] inputsTrain= inputs.Get(0, traintPos);
            double[][] inputsTest = inputs.Get(traintPos, testPos);

По аналогии с целевой функцией создаем массивы входных данных (признаков).

//simple linear regression model
            var ols = new OrdinaryLeastSquares()
            {
                UseIntercept = true
            };

            //linear regression model for several features
            MultipleLinearRegression regression = ols.Learn(inputsTrain, OutResPositiveTrain);

Осталось создать модели. Вначале создаем объект обычной линейной регрессии, а потом на основе него создаем модель для множественной регрессии ведь у нас почти 30 признаков. Обучаем модель естественно на тренировочной выборке.

            //make a prediction
            double[] predicted = regression.Transform(inputsTest);

Получаем непосредственно предсказание для тренировочной выборки.

  //console output

            for (int i = 0; i < testPos - traintPos; i++)
            {
                Console.WriteLine("predicted: {0}   real: {1}", predicted[i], OutResPositiveTest[i]);
            }
            // And  print the squared error using the SquareLoss class:
            Console.WriteLine("error = {0}", new SquareLoss(OutResPositiveTest).Loss(predicted));

            // print the coefficient of determination
            double r2 = new RSquaredLoss(numberOfInputs: 29, expected: OutResPositiveTest).Loss(predicted); 
            Console.WriteLine("R^2 = {0}", r2);

            // alternative print the coefficient of determination
            double ur2 = regression.CoefficientOfDetermination(inputs, OutResPositiveTest, adjust: true);
            Console.WriteLine("alternative version of R2 = {0}", r2);

            Console.WriteLine("Press enter and close chart to exit");

Выводим в консоль данные о спрогнозированном и реальном значениях, а также сведения об ошибке и коэффициенте детерминации.

// for chart 

            int[] classes = new int[allData];
            double[] mountX = new double[allData];
            for (int i = 0; i < allData; i++)
            {
                if (i<testPos)
                {
                   // for csv data
                    mountX[i] = i+1;
                    classes[i] = 0; //csv data is class 0
                }
                else
                {
                    //for predicted
                    mountX[i] = i- (testPos - traintPos)+1;
                    classes[i] = 1; //predicted is class 1
                }

                
            }

            // make points of chart
            List<double> OutChart = new List<double>();
            OutChart.AddRange(OutResPositive);
            OutChart.AddRange(predicted);

Разработчики похоже сами советуют использовать сторонние инструменты для отображения графиков, но мы воспользуемся поставляемым с фреймворком графиком ScatterplotBox, который выводит точки. Чтобы данные были хоть как-то наглядны мы по шкале X создаем аналог временного тренда (точка 1 это января 16, последняя точка октябрь 2017), также параллельно мы классифицируем точки в другом массиве первые 22 это наши исходные данные, а последние 4 предсказанные (график раскрасит их в другой цвет).

            // plot chart
            ScatterplotBox.Show("res_positive from months", mountX, OutChart.ToArray(), classes).Hold();

            // for pause
            Console.ReadLine();
        }
    }
}

ScatterplotBox.Show выводит окошко с графиком. Ему мы скормим наши ранее подготовленные данные для осей Х и У.

Честно признаюсь Visual Basic я не знаю, но тут нам поможет конвертер с C# на VB.NET.

Разбирать по частям код не будем, вы можете ориентироваться по оставленным в коде комментариям, они идентичны для обоих проектов и делят код на аналогичные секции.

Полный код на VB.NET
Imports System
Imports System.Linq
Imports Accord.Statistics.Models.Regression.Linear
Imports Accord.IO
Imports Accord.Math
Imports System.Data
Imports System.Collections.Generic
Imports Accord.Controls
Imports Accord.Math.Optimization.Losses

Module Program
    Sub Main()
        'for separating the training and test samples
        Dim traintPos As Integer = 18
        Dim testPos As Integer = 22
        Dim allData As Integer = testPos + (testPos - traintPos)

        'for correct reading symbol of float point in csv
        Dim customCulture As System.Globalization.CultureInfo = CType(System.Threading.Thread.CurrentThread.CurrentCulture.Clone(), System.Globalization.CultureInfo)
        customCulture.NumberFormat.NumberDecimalSeparator = "."
        System.Threading.Thread.CurrentThread.CurrentCulture = customCulture

        'read data
        Dim CsvFilePath As String = "msc_appel_data.csv"
        Dim mscTable As DataTable = New CsvReader(CsvFilePath, True).ToTable()

        'for encoding the string values of months into numerical values
        Dim monthNames As Dictionary(Of String, Double) = New Dictionary(Of String, Double) From
            {{"January", 1}, {"February", 2}, {"March", 3}, {"April", 4}, {"May", 5}, {"June", 6},
            {"July", 7}, {"August", 8}, {"September", 9},
            {"October", 10}, {"November", 11}, {"December", 12}}
        Dim months As String() = mscTable.Columns("month").ToArray(Of String)()
        Dim dMonths As Double() = New Double(months.Length - 1) {}
        For i As Integer = 0 To months.Length - 1
            dMonths(i) = monthNames(months(i))
        Next

        'select the target column
        Dim OutResPositive As Double() = mscTable.Columns("res_positive").ToArray()

        'separation of the test and train target sample
        Dim OutResPositiveTrain As Double() = OutResPositive.[Get](0, traintPos)
        Dim OutResPositiveTest As Double() = OutResPositive.[Get](traintPos, testPos)

        'deleting unneeded columns
        mscTable.Columns.Remove("total_appeals")
        mscTable.Columns.Remove("month")
        mscTable.Columns.Remove("res_positive")
        mscTable.Columns.Remove("year")

        'add coded in a double column month into Table
        'create new column

        Dim newCol As DataColumn = New DataColumn("dMonth", GetType(Double))
        newCol.AllowDBNull = True

        'add new column
        mscTable.Columns.Add(newCol)

        'fill new column
        Dim counter As Integer = 0
        For Each row As DataRow In mscTable.Rows
            row("dMonth") = dMonths(counter)
            counter += 1
        Next


        'receiving input data from a table
        Dim inputs As Double()() = mscTable.ToArray()

        'separation of the test and train sample
        Dim inputsTrain As Double()() = inputs.[Get](0, traintPos)
        Dim inputsTest As Double()() = inputs.[Get](traintPos, testPos)

        'simple linear regression model
        Dim ols = New OrdinaryLeastSquares() With {.UseIntercept = True}

        'linear regression model for several features
        Dim regression As MultipleLinearRegression = ols.Learn(inputsTrain, OutResPositiveTrain)

        'make a prediction
        Dim predicted As Double() = regression.Transform(inputsTest)

        'console output
        For i As Integer = 0 To testPos - traintPos - 1
            Console.WriteLine("predicted: {0}   real: {1}", predicted(i), OutResPositiveTest(i))
        Next

        'And  print the squared error using the SquareLoss class
        Console.WriteLine("error = {0}", New SquareLoss(OutResPositiveTest).Loss(predicted))

        'print the coefficient of determination
        Dim r2 As Double = New RSquaredLoss(numberOfInputs:=29, expected:=OutResPositiveTest).Loss(predicted)
        Console.WriteLine("R^2 = {0}", r2)

        'alternative print the coefficient of determination
        Dim ur2 As Double = regression.CoefficientOfDetermination(inputs, OutResPositiveTest, adjust:=True)
        Console.WriteLine("alternative version of R2 = {0}", r2)


        Console.WriteLine("Press enter and close chart to exit")

        'for chart 
        Dim classes As Integer() = New Integer(allData - 1) {}
        Dim mountX As Double() = New Double(allData - 1) {}
        For i As Integer = 0 To allData - 1
            If i < testPos Then
                mountX(i) = i + 1
                classes(i) = 0 'csv data is class 0
            Else
                mountX(i) = i - (testPos - traintPos) + 1
                classes(i) = 1 'predicted is class 1
            End If
        Next

        'make points of chart
        Dim OutChart As List(Of Double) = New List(Of Double)()
        OutChart.AddRange(OutResPositive)
        OutChart.AddRange(predicted)

        'plot chart
        ScatterplotBox.Show("res_positive from months", mountX, OutChart.ToArray(), classes).Hold()

        'for pause
        Console.ReadLine()
    End Sub

End Module



Надо отметить, что наш проект получился вполне кроссплатформенным, поскольку его можно собрать как с помощью Visual Studio под Windows, так и с помощью MonoDevelop под Linux. Правда это справедливо, только по отношению к C#, код на VB.NET под Mono не всегда собирается без проблем.
Вместо тысячи слов лучше посмотрим на снимки экрана.

Сборка VB проекта версии 1.0.1. под Windows.



Сборка С# проекта версии 1.0.0. под Linux Mint.



Вы, наверное, обратили внимание, что результаты на картинках немного различаются.
Это не вина Mono. Всё дело в том, что в версии проекта (1.0.0) на C# собранной под Linux я забыл учесть перекодированный столбец с месяцами. А в версии проекта (1.0.1) на VB собранной в Visual Studio — учел.

Хотел вначале поправить снимки экрана, но потом подумал, что это — наглядная демонстрация того, что данный признак чуть-чуть улучшает качество предсказания.

Однако, на самом деле мы добились плохих результатов, не имеющих никакой пользы кроме учебной.

Причиной этому стали следующие факторы:

  1. Данные у нас в разных величинах, но мы их при этом не масштабировали. (Потому что я не разобрался еще как это сделать с помощью Accord.NET).
  2. Также мы запихнули в модель почти все признаки и при этом не использовали отсев «плохих» признаков, то есть регуляризацию.(Угадайте почему? Правильно потому что я тоже пока не разобрался с ней).
  3. Ну и безусловно слишком мало данных чтобы делать, нормальные предсказания.

Может быть еще какие-то вещи, о которых мне неизвестно.

Но мы к счастью и не ставили себе целью практическое применение модели, нам было важно узнать о существовании фреймворка и попробовать сделать простейшие вещи, ну а дальше я надеюсь, что вы освоите этот инструмент и уже я буду на ваших статьях учится работе с Accord.Net.
Tags:
Hubs:
+15
Comments 4
Comments Comments 4

Articles