text:
try:
    import unittest2 as unittest
except ImportError:
    import unittest  # noqa

from binascii import unhexlify
from mock import Mock
import os
import six
import timeit

import cassandra
from cassandra.marshal import uint16_unpack, uint16_pack
from cassandra.metadata import (Murmur3Token, MD5Token,
                                BytesToken, ReplicationStrategy,
                                NetworkTopologyStrategy, SimpleStrategy,
                                LocalStrategy, protect_name,
                                protect_names, protect_value, is_valid_name,
                                UserType, KeyspaceMetadata, get_schema_parser,
                                _UnknownStrategy, ColumnMetadata, TableMetadata,
                                IndexMetadata, Function, Aggregate,
                                Metadata)
from cassandra.policies import SimpleConvictionPolicy
from cassandra.pool import Host

class StrategiesTest(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        "Hook method for setting up class fixture before running tests in the class."
        if not hasattr(cls, 'assertItemsEqual'):
            cls.assertItemsEqual = cls.assertCountEqual

    def test_replication_strategy(self):
        """
        Basic code coverage testing that ensures different ReplicationStrategies
        can be initiated using parameters correctly.
        """

        rs = ReplicationStrategy()

        self.assertEqual(rs.create('OldNetworkTopologyStrategy', None), _UnknownStrategy('OldNetworkTopologyStrategy', None))
        fake_options_map = {'options': 'map'}
        uks = rs.create('OldNetworkTopologyStrategy', fake_options_map)
        self.assertEqual(uks, _UnknownStrategy('OldNetworkTopologyStrategy', fake_options_map))
        self.assertEqual(uks.make_token_replica_map({}, []), {})

        fake_options_map = {'dc1': '3'}
        self.assertIsInstance(rs.create('NetworkTopologyStrategy', fake_options_map), NetworkTopologyStrategy)
        self.assertEqual(rs.create('NetworkTopologyStrategy', fake_options_map).dc_replication_factors,
                         NetworkTopologyStrategy(fake_options_map).dc_replication_factors)

        fake_options_map = {'options': 'map'}
        self.assertIsNone(rs.create('SimpleStrategy', fake_options_map))

        fake_options_map = {'options': 'map'}
        self.assertIsInstance(rs.create('LocalStrategy', fake_options_map), LocalStrategy)

        fake_options_map = {'options': 'map', 'replication_factor': 3}
        self.assertIsInstance(rs.create('SimpleStrategy', fake_options_map), SimpleStrategy)
        self.assertEqual(rs.create('SimpleStrategy', fake_options_map).replication_factor,
                         SimpleStrategy(fake_options_map).replication_factor)

        self.assertEqual(rs.create('xxxxxxxx', fake_options_map), _UnknownStrategy('xxxxxxxx', fake_options_map))

        self.assertRaises(NotImplementedError, rs.make_token_replica_map, None, None)
        self.assertRaises(NotImplementedError, rs.export_for_schema)

    def test_nts_make_token_replica_map(self):
        token_to_host_owner = {}

        dc1_1 = Host('dc1.1', SimpleConvictionPolicy)
        dc1_2 = Host('dc1.2', SimpleConvictionPolicy)
        dc1_3 = Host('dc1.3', SimpleConvictionPolicy)
        for host in (dc1_1, dc1_2, dc1_3):
            host.set_location_info('dc1', 'rack1')
        token_to_host_owner[MD5Token(0)] = dc1_1
        token_to_host_owner[MD5Token(100)] = dc1_2
        token_to_host_owner[MD5Token(200)] = dc1_3

        dc2_1 = Host('dc2.1', SimpleConvictionPolicy)
        dc2_2 = Host('dc2.2', SimpleConvictionPolicy)
        dc2_1.set_location_info('dc2', 'rack1')
        dc2_2.set_location_info('dc2', 'rack1')
        token_to_host_owner[MD5Token(1)] = dc2_1
        token_to_host_owner[MD5Token(101)] = dc2_2

        dc3_1 = Host('dc3.1', SimpleConvictionPolicy)
        dc3_1.set_location_info('dc3', 'rack3')
        token_to_host_owner[MD5Token(2)] = dc3_1

        ring = [MD5Token(0),
                MD5Token(1),
                MD5Token(2),
                MD5Token(100),
                MD5Token(101),
                MD5Token(200)]

        nts = NetworkTopologyStrategy({'dc1': 2, 'dc2': 2, 'dc3': 1})
        replica_map = nts.make_token_replica_map(token_to_host_owner, ring)

        self.assertItemsEqual(replica_map[MD5Token(0)], (dc1_1, dc1_2, dc2_1, dc2_2, dc3_1))

    def test_nts_token_performance(self):
        """
        Tests to ensure that when rf exceeds the number of nodes available, that we dont'
        needlessly iterate trying to construct tokens for nodes that don't exist.

        @since 3.7
        @jira_ticket PYTHON-379
        @expected_result timing with 1500 rf should be same/similar to 3rf if we have 3 nodes

        @test_category metadata
        """

        token_to_host_owner = {}
        ring = []
        dc1hostnum = 3
        current_token = 0
        vnodes_per_host = 500
        for i in range(dc1hostnum):

            host = Host('dc1.{0}'.format(i), SimpleConvictionPolicy)
            host.set_location_info('dc1', "rack1")
            for vnode_num in range(vnodes_per_host):
                md5_token = MD5Token(current_token+vnode_num)
                token_to_host_owner[md5_token] = host
                ring.append(md5_token)
            current_token += 1000

        nts = NetworkTopologyStrategy({'dc1': 3})
        start_time = timeit.default_timer()
        nts.make_token_replica_map(token_to_host_owner, ring)
        elapsed_base = timeit.default_timer() - start_time

        nts = NetworkTopologyStrategy({'dc1': 1500})
        start_time = timeit.default_timer()
        nts.make_token_replica_map(token_to_host_owner, ring)
        elapsed_bad = timeit.default_timer() - start_time
        difference = elapsed_bad - elapsed_base
        self.assertTrue(difference < 1 and difference > -1)

    def test_nts_make_token_replica_map_multi_rack(self):
        token_to_host_owner = {}

        # (A) not enough distinct racks, first skipped is used
        dc1_1 = Host('dc1.1', SimpleConvictionPolicy)
        dc1_2 = Host('dc1.2', SimpleConvictionPolicy)
        dc1_3 = Host('dc1.3', SimpleConvictionPolicy)
        dc1_4 = Host('dc1.4', SimpleConvictionPolicy)
        dc1_1.set_location_info('dc1', 'rack1')
        dc1_2.set_location_info('dc1', 'rack1')
        dc1_3.set_location_info('dc1', 'rack2')
        dc1_4.set_location_info('dc1', 'rack2')
        token_to_host_owner[MD5Token(0)] = dc1_1
        token_to_host_owner[MD5Token(100)] = dc1_2
        token_to_host_owner[MD5Token(200)] = dc1_3
        token_to_host_owner[MD5Token(300)] = dc1_4

        # (B) distinct racks, but not contiguous
        dc2_1 = Host('dc2.1', SimpleConvictionPolicy)
        dc2_2 = Host('dc2.2', SimpleConvictionPolicy)
        dc2_3 = Host('dc2.3', SimpleConvictionPolicy)
        dc2_1.set_location_info('dc2', 'rack1')
        dc2_2.set_location_info('dc2', 'rack1')
        dc2_3.set_location_info('dc2', 'rack2')
        token_to_host_owner[MD5Token(1)] = dc2_1
        token_to_host_owner[MD5Token(101)] = dc2_2
        token_to_host_owner[MD5Token(201)] = dc2_3

        ring = [MD5Token(0),
                MD5Token(1),
                MD5Token(100),
                MD5Token(101),
                MD5Token(200),
                MD5Token(201),
                MD5Token(300)]

        nts = NetworkTopologyStrategy({'dc1': 3, 'dc2': 2})
        replica_map = nts.make_token_replica_map(token_to_host_owner, ring)

        token_replicas = replica_map[MD5Token(0)]
        self.assertItemsEqual(token_replicas, (dc1_1, dc1_2, dc1_3, dc2_1, dc2_3))

    def test_nts_make_token_replica_map_empty_dc(self):
        host = Host('1', SimpleConvictionPolicy)
        host.set_location_info('dc1', 'rack1')
        token_to_host_owner = {MD5Token(0): host}
        ring = [MD5Token(0)]
        nts = NetworkTopologyStrategy({'dc1': 1, 'dc2': 0})

        replica_map = nts.make_token_replica_map(token_to_host_owner, ring)
        self.assertEqual(set(replica_map[MD5Token(0)]), set([host]))

    def test_nts_export_for_schema(self):
        strategy = NetworkTopologyStrategy({'dc1': '1', 'dc2': '2'})
        self.assertEqual("{'class': 'NetworkTopologyStrategy', 'dc1': '1', 'dc2': '2'}",
                         strategy.export_for_schema())

    def test_simple_strategy_make_token_replica_map(self):
        host1 = Host('1', SimpleConvictionPolicy)
        host2 = Host('2', SimpleConvictionPolicy)
        host3 = Host('3', SimpleConvictionPolicy)
        token_to_host_owner = {
            MD5Token(0): host1,
            MD5Token(100): host2,
            MD5Token(200): host3
        }
        ring = [MD5Token(0), MD5Token(100), MD5Token(200)]

        rf1_replicas = SimpleStrategy({'replication_factor': '1'}).make_token_replica_map(token_to_host_owner, ring)
        self.assertItemsEqual(rf1_replicas[MD5Token(0)], [host1])
        self.assertItemsEqual(rf1_replicas[MD5Token(100)], [host2])
        self.assertItemsEqual(rf1_replicas[MD5Token(200)], [host3])

        rf2_replicas = SimpleStrategy({'replication_factor': '2'}).make_token_replica_map(token_to_host_owner, ring)
        self.assertItemsEqual(rf2_replicas[MD5Token(0)], [host1, host2])
        self.assertItemsEqual(rf2_replicas[MD5Token(100)], [host2, host3])
        self.assertItemsEqual(rf2_replicas[MD5Token(200)], [host3, host1])

        rf3_replicas = SimpleStrategy({'replication_factor': '3'}).make_token_replica_map(token_to_host_owner, ring)
        self.assertItemsEqual(rf3_replicas[MD5Token(0)], [host1, host2, host3])
        self.assertItemsEqual(rf3_replicas[MD5Token(100)], [host2, host3, host1])
        self.assertItemsEqual(rf3_replicas[MD5Token(200)], [host3, host1, host2])

    def test_ss_equals(self):
        self.assertNotEqual(SimpleStrategy({'replication_factor': '1'}), NetworkTopologyStrategy({'dc1': 2}))

class NameEscapingTest(unittest.TestCase):

    def test_protect_name(self):
        """
        Test cassandra.metadata.protect_name output
        """
        self.assertEqual(protect_name('tests'), 'tests')
        self.assertEqual(protect_name('test\'s'), '"test\'s"')
        self.assertEqual(protect_name('test\'s'), "\"test's\"")
        self.assertEqual(protect_name('tests ?!@#$%^&*()'), '"tests ?!@#$%^&*()"')
        self.assertEqual(protect_name('1'), '"1"')
        self.assertEqual(protect_name('1test'), '"1test"')

    def test_protect_names(self):
        """
        Test cassandra.metadata.protect_names output
        """
        self.assertEqual(protect_names(['tests']), ['tests'])
        self.assertEqual(protect_names(
            [
                'tests',
                'test\'s',
                'tests ?!@#$%^&*()',
                '1'
            ]),
            [
                'tests',
                "\"test's\"",
                '"tests ?!@#$%^&*()"',
                '"1"'
            ])

    def test_protect_value(self):
        """
        Test cassandra.metadata.protect_value output
        """
        self.assertEqual(protect_value(True), "true")
        self.assertEqual(protect_value(False), "false")
        self.assertEqual(protect_value(3.14), '3.14')
        self.assertEqual(protect_value(3), '3')
        self.assertEqual(protect_value('test'), "'test'")
        self.assertEqual(protect_value('test\'s'), "'test''s'")
        self.assertEqual(protect_value(None), 'NULL')

    def test_is_valid_name(self):
        """
        Test cassandra.metadata.is_valid_name output
        """
        self.assertEqual(is_valid_name(None), False)
        self.assertEqual(is_valid_name('test'), True)
        self.assertEqual(is_valid_name('Test'), False)
        self.assertEqual(is_valid_name('t_____1'), True)
        self.assertEqual(is_valid_name('test1'), True)
        self.assertEqual(is_valid_name('1test1'), False)

        invalid_keywords = cassandra.metadata.cql_keywords - cassandra.metadata.cql_keywords_unreserved
        for keyword in invalid_keywords:
            self.assertEqual(is_valid_name(keyword), False)

class Murmur3TokensTest(unittest.TestCase):

    def test_murmur3_init(self):
        murmur3_token = Murmur3Token(cassandra.metadata.MIN_LONG - 1)
        self.assertEqual(str(murmur3_token), '<Murmur3Token: -9223372036854775809>')

    def test_python_vs_c(self):
        from cassandra.murmur3 import _murmur3 as mm3_python
        try:
            from cassandra.cmurmur3 import murmur3 as mm3_c

            iterations = 100
            for _ in range(iterations):
                for len in range(0, 32):  # zero to one block plus full range of tail lengths
                    key = os.urandom(len)
                    self.assertEqual(mm3_python(key), mm3_c(key))

        except ImportError:
            raise unittest.SkipTest('The cmurmur3 extension is not available')

    def test_murmur3_python(self):
        from cassandra.murmur3 import _murmur3
        self._verify_hash(_murmur3)

    def test_murmur3_c(self):
        try:
            from cassandra.cmurmur3 import murmur3
            self._verify_hash(murmur3)
        except ImportError:
            raise unittest.SkipTest('The cmurmur3 extension is not available')

    def _verify_hash(self, fn):
        self.assertEqual(fn(six.b('123')), -7468325962851647638)
        self.assertEqual(fn(b'\x00\xff\x10\xfa\x99' * 10), 5837342703291459765)
        self.assertEqual(fn(b'\xfe' * 8), -8927430733708461935)
        self.assertEqual(fn(b'\x10' * 8), 1446172840243228796)
        self.assertEqual(fn(six.b(str(cassandra.metadata.MAX_LONG))), 7162290910810015547)

class MD5TokensTest(unittest.TestCase):

    def test_md5_tokens(self):
        md5_token = MD5Token(cassandra.metadata.MIN_LONG - 1)
        self.assertEqual(md5_token.hash_fn('123'), 42767516990368493138776584305024125808)
        self.assertEqual(md5_token.hash_fn(str(cassandra.metadata.MAX_LONG)), 28528976619278518853815276204542453639)
        self.assertEqual(str(md5_token), '<MD5Token: %s>' % -9223372036854775809)

class BytesTokensTest(unittest.TestCase):

    def test_bytes_tokens(self):
        bytes_token = BytesToken(unhexlify(six.b('01')))
        self.assertEqual(bytes_token.value, six.b('\x01'))
        self.assertEqual(str(bytes_token), "<BytesToken: %s>" % bytes_token.value)
        self.assertEqual(bytes_token.hash_fn('123'), '123')
        self.assertEqual(bytes_token.hash_fn(123), 123)
        self.assertEqual(bytes_token.hash_fn(str(cassandra.metadata.MAX_LONG)), str(cassandra.metadata.MAX_LONG))

    def test_from_string(self):
        from_unicode = BytesToken.from_string(six.text_type('0123456789abcdef'))
        from_bin = BytesToken.from_string(six.b('0123456789abcdef'))
        self.assertEqual(from_unicode, from_bin)
        self.assertIsInstance(from_unicode.value, six.binary_type)
        self.assertIsInstance(from_bin.value, six.binary_type)

    def test_comparison(self):
        tok = BytesToken.from_string(six.text_type('0123456789abcdef'))
        token_high_order = uint16_unpack(tok.value[0:2])
        self.assertLess(BytesToken(uint16_pack(token_high_order - 1)), tok)
        self.assertGreater(BytesToken(uint16_pack(token_high_order + 1)), tok)

    def test_comparison_unicode(self):
        value = six.b('\'_-()"\xc2\xac')
        t0 = BytesToken(value)
        t1 = BytesToken.from_string('00')
        self.assertGreater(t0, t1)
        self.assertFalse(t0 < t1)

class KeyspaceMetadataTest(unittest.TestCase):

    def test_export_as_string_user_types(self):
        keyspace_name = 'test'
        keyspace = KeyspaceMetadata(keyspace_name, True, 'SimpleStrategy', dict(replication_factor=3))
        keyspace.user_types['a'] = UserType(keyspace_name, 'a', ['one', 'two'], ['c', 'int'])
        keyspace.user_types['b'] = UserType(keyspace_name, 'b', ['one', 'two', 'three'], ['d', 'int', 'a'])
        keyspace.user_types['c'] = UserType(keyspace_name, 'c', ['one'], ['int'])
        keyspace.user_types['d'] = UserType(keyspace_name, 'd', ['one'], ['c'])

        self.assertEqual("""CREATE KEYSPACE test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '3'}  AND durable_writes = true;

CREATE TYPE test.c (
    one int
);

CREATE TYPE test.a (
    one c,
    two int
);

CREATE TYPE test.d (
    one c
);

CREATE TYPE test.b (
    one d,
    two int,
    three a
);""", keyspace.export_as_string())

class UserTypesTest(unittest.TestCase):

    def test_as_cql_query(self):
        field_types = ['varint', 'ascii', 'frozen<tuple<varint, ascii>>']
        udt = UserType("ks1", "mytype", ["a", "b", "c"], field_types)
        self.assertEqual("CREATE TYPE ks1.mytype (a varint, b ascii, c frozen<tuple<varint, ascii>>)", udt.as_cql_query(formatted=False))

        self.assertEqual("""CREATE TYPE ks1.mytype (
    a varint,
    b ascii,
    c frozen<tuple<varint, ascii>>
);""", udt.export_as_string())

    def test_as_cql_query_name_escaping(self):
        udt = UserType("MyKeyspace", "MyType", ["AbA", "keyspace"], ['ascii', 'ascii'])
        self.assertEqual('CREATE TYPE "MyKeyspace"."MyType" ("AbA" ascii, "keyspace" ascii)', udt.as_cql_query(formatted=False))

class IndexTest(unittest.TestCase):

    def test_build_index_as_cql(self):
        column_meta = Mock()
        column_meta.name = 'column_name_here'
        column_meta.table.name = 'table_name_here'
        column_meta.table.keyspace_name = 'keyspace_name_here'
        column_meta.table.columns = {column_meta.name: column_meta}
        parser = get_schema_parser(Mock(), '2.1.0', 0.1)

        row = {'index_name': 'index_name_here', 'index_type': 'index_type_here'}
        index_meta = parser._build_index_metadata(column_meta, row)
        self.assertEqual(index_meta.as_cql_query(),
                'CREATE INDEX index_name_here ON keyspace_name_here.table_name_here (column_name_here)')

        row['index_options'] = '{ "class_name": "class_name_here" }'
        row['index_type'] = 'CUSTOM'
        index_meta = parser._build_index_metadata(column_meta, row)
        self.assertEqual(index_meta.as_cql_query(),
                "CREATE CUSTOM INDEX index_name_here ON keyspace_name_here.table_name_here (column_name_here) USING 'class_name_here'")

class UnicodeIdentifiersTests(unittest.TestCase):
    """
    Exercise cql generation with unicode characters. Keyspace, Table, and Index names
    cannot have special chars because C* names files by those identifiers, but they are
    tested anyway.

    Looking for encoding errors like PYTHON-447
    """

    name = six.text_type(b'\'_-()"\xc2\xac'.decode('utf-8'))

    def test_keyspace_name(self):
        km = KeyspaceMetadata(self.name, False, 'SimpleStrategy', {'replication_factor': 1})
        km.export_as_string()

    def test_table_name(self):
        tm = TableMetadata(self.name, self.name)
        tm.export_as_string()

    def test_column_name_single_partition(self):
        tm = TableMetadata('ks', 'table')
        cm = ColumnMetadata(tm, self.name, u'int')
        tm.columns[cm.name] = cm
        tm.partition_key.append(cm)
        tm.export_as_string()

    def test_column_name_single_partition_single_clustering(self):
        tm = TableMetadata('ks', 'table')
        cm = ColumnMetadata(tm, self.name, u'int')
        tm.columns[cm.name] = cm
        tm.partition_key.append(cm)
        cm = ColumnMetadata(tm, self.name + 'x', u'int')
        tm.columns[cm.name] = cm
        tm.clustering_key.append(cm)
        tm.export_as_string()

    def test_column_name_multiple_partition(self):
        tm = TableMetadata('ks', 'table')
        cm = ColumnMetadata(tm, self.name, u'int')
        tm.columns[cm.name] = cm
        tm.partition_key.append(cm)
        cm = ColumnMetadata(tm, self.name + 'x', u'int')
        tm.columns[cm.name] = cm
        tm.partition_key.append(cm)
        tm.export_as_string()

    def test_index(self):
        im = IndexMetadata(self.name, self.name, self.name, kind='', index_options={'target': self.name})
        im.export_as_string()
        im = IndexMetadata(self.name, self.name, self.name, kind='CUSTOM', index_options={'target': self.name, 'class_name': 'Class'})
        im.export_as_string()

    def test_function(self):
        fm = Function(self.name, self.name, (u'int', u'int'), (u'x', u'y'), u'int', u'language', self.name, False)
        fm.export_as_string()

    def test_aggregate(self):
        am = Aggregate(self.name, self.name, (u'text',), self.name, u'text', self.name, self.name, u'text')
        am.export_as_string()

    def test_user_type(self):
        um = UserType(self.name, self.name, [self.name, self.name], [u'int', u'text'])
        um.export_as_string()

class HostsTests(unittest.TestCase):
    def test_iterate_all_hosts_and_modify(self):
        """
        PYTHON-572
        """
        metadata = Metadata()
        metadata.add_or_return_host(Host('dc1.1', SimpleConvictionPolicy))
        metadata.add_or_return_host(Host('dc1.2', SimpleConvictionPolicy))

        self.assertEqual(len(metadata.all_hosts()), 2)

        for host in metadata.all_hosts():  # this would previously raise in Py3
            metadata.remove_host(host)

        self.assertEqual(len(metadata.all_hosts()), 0)


text:
SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
RESET=reset_magi.sh
FABFILE=tbutil_fabfile.py
POOLSIZE=15

if [[ ! -f ${SCRIPTDIR}/${RESET} ]]; then
	echo Could not locate $RESET script.
	exit 2
fi

# We only have a few extra args in addition to the reset args, if we don't recognize an arg, we'll assume it goes
# to reset_magi
while getopts :e:v:h opt; do
	case $opt in
	e) PIDEID=$OPTARG
	    ;;
	v) MAGI_VERSION=$OPTARG
	    ;;
        *)
            ;;
   esac
done

PID=$(echo ${PIDEID} | cut -d, -f1)
EID=$(echo ${PIDEID} | cut -d, -f2)

if [[ -z ${PID} ]]; then
    PID=$(echo ${PIDEID} | cut -d/ -f1)
    EID=$(echo ${PIDEID} | cut -d/ -f2)
fi

if [[ -z ${PID} ]]; then
    echo Error reading experiment name. Use -e pid,eid or set \$EXP in the environment.
    exit 2
fi

# Do this first in case we're moving to a different magi version.
#${SCRIPTDIR}/${RESET} $@

MAGI_DIR=/proj/edgect/magi/${MAGI_VERSION}

if [[ ! -d ${MAGI_DIR} ]]; then
    echo Error determining location of magi version \(${MAGI_VERSION}\) specified.
    echo -e "\t${MAGI_DIR} does not exist."
    exit 2
fi

fab -f ${SCRIPTDIR}/${FABFILE} \
    --pool-size=${POOLSIZE} \
    initenv:pid=${PID},eid=${EID} \
    magi_log_to_space:mdir=${MAGI_DIR}

redpajama_set_name: RedPajamaGithub


text:
using Eto.Drawing;
using Eto.Forms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using swf = System.Windows.Forms;
using sd = System.Drawing;

namespace Eto.WinForms.Forms.Controls
{
	public class ColorPickerHandler : WindowsControl<swf.Button, ColorPicker, ColorPicker.ICallback>, ColorPicker.IHandler
	{
		public ColorPickerHandler()
		{
			Control = new swf.Button { Width = 40 };
			Control.Click += HandleClick;
			Control.BackColor = sd.Color.Black;
		}

		public override Color BackgroundColor
		{
			get { return Colors.Transparent; }
			set
			{
				// cannot set background color, we use that as the currently selected color
			}
		}

		public override void AttachEvent(string id)
		{
			switch (id)
			{
				case ColorPicker.ColorChangedEvent:
					break;
				default:
					base.AttachEvent(id);
					break;
			}
		}

		void HandleClick(object sender, EventArgs e)
		{
			using (Widget.Platform.Context)
			{
				var picker = new ColorDialog { Color = Color };
				var result = picker.ShowDialog(Widget);
				if (result == DialogResult.Ok)
				{
					Color = picker.Color;
				}
			}
		}

		public Eto.Drawing.Color Color
		{
			get { return Control.BackColor.ToEto(); }
			set
			{
				var color = value.ToSD();
				if (Control.BackColor != color)
				{
					Control.BackColor = color;
					Callback.OnColorChanged(Widget, EventArgs.Empty);
				}
			}
		}

		static readonly Win32.WM[] intrinsicEvents = { Win32.WM.LBUTTONDOWN, Win32.WM.LBUTTONUP, Win32.WM.LBUTTONDBLCLK };
		public override bool ShouldBubbleEvent(swf.Message msg)
		{
			return !intrinsicEvents.Contains((Win32.WM)msg.Msg) && base.ShouldBubbleEvent(msg);
		}
	}
}


text:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>com.cjmalloy</groupId>
    <artifactId>torrent-fs</artifactId>
    <version>0.3-SNAPSHOT</version>
    <relativePath>../pom.xml</relativePath>
  </parent>

  <artifactId>torrent-fs-standalone</artifactId>
  <packaging>jar</packaging>

  <name>torrent-fs-standalone</name>

  <dependencies>
    <dependency>
      <groupId>com.cjmalloy</groupId>
      <artifactId>torrent-fs-server</artifactId>
      <version>0.3-SNAPSHOT</version>
    </dependency>

    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-servlet</artifactId>
      <version>9.2.3.v20140905</version>
    </dependency>
    <dependency>
      <groupId>org.teleal.cling</groupId>
      <artifactId>cling-support</artifactId>
      <version>1.0.5</version>
    </dependency>
    <dependency>
      <groupId>net.sf</groupId>
      <artifactId>jargs</artifactId>
      <version>1.0</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.24</version>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.17</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>torrent-fs</finalName>

    <plugins>
      <plugin>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-maven-plugin</artifactId>
        <version>9.2.3.v20140905</version>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.3</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <createDependencyReducedPom>false</createDependencyReducedPom>
              <filters>
                <filter>
                  <artifact>*:*</artifact>
                  <excludes>
                    <exclude>META-INF/*.SF</exclude>
                    <exclude>META-INF/*.DSA</exclude>
                    <exclude>META-INF/*.RSA</exclude>
                  </excludes>
                </filter>
              </filters>
              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>com.cjmalloy.torrentfs.server.Entry</mainClass>
                </transformer>
                <!--  use transformer to handle merge of META-INF/services -->
                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

  <repositories>
    <repository>
      <id>teleal-repo</id>
      <url>http://teleal.org/m2</url>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </repository>
    <repository>
      <id>jboss-thirdparty-releases</id>
      <name>JBoss Thirdparty Releases</name>
      <url>https://repository.jboss.org/nexus/content/repositories/thirdparty-releases/</url>
    </repository>
</repositories>
</project>


text:
layout: post
title: "Customer Churn Modeling using Machine Learning with parsnip"
date:   2019-11-18 10:44:01
excerpt: "Learn how to perform a tidy approach to classification problem with the new parsnip R package for machine learning."
author: "Diego Usai"
categories: [Code-Tools]
tags: [R-Bloggers, Learn-R, Learn-Machine-Learning, Learn-Marketing, parsnip, rsample, recipes, yardstick, ranger, skimr]
image: /assets/2019-11-18-parsnip/parsnip_workflow.jpg
image_preview: /assets/2019-11-18-parsnip/parsnip_workflow.jpg
---

This article comes from Diego Usai, a student in Business Science University. Diego has completed both  101 (Data Science Foundations) and 201 (Advanced Machine Learning & Business Consulting) courses. Diego shows off his progress in this Customer Churn Tutorial using Machine Learning with parsnip. Diego originally posted the article on his personal website, diegousai.io, which has been reproduced on the Business Science blog here. Enjoy!

__R Packages Covered__:

* `parsnip` - NEW Machine Learning API in R, similar to `scikit learn` in Python
* `rsample` - 10-Fold Cross Validation
* `recipes` - Data preprocessing
* `yardstick` - Model scoring and metrics
* `skimr` - Quickly skim data
* `ranger` - Random Forest Library used for churn modeling

# Churn Modeling Using Machine Learning

by Diego Usai, Customer Insights Consultant

Recently I have completed the online course [__Business Analysis With R__](https://university.business-science.io/p/ds4b-101-r-business-analysis-r) focused on __applied data and business science with R__, which introduced me to a couple of new modelling concepts and approaches. One that especially captured my attention is `parsnip` and its attempt to implement a unified modelling and analysis interface (similar to python's `scikit-learn`) to seamlessly access several modelling platforms in R.

`parsnip` is the brainchild of RStudio's [Max Khun](https://twitter.com/topepos) (of `caret` fame) and [Davis Vaughan](https://twitter.com/dvaughan32) and forms part of `tidymodels`, a growing ensemble of tools to explore and iterate modelling tasks that shares a common philosophy (and a few libraries) with the `tidyverse`.

Although there are a number of packages at different stages in their development, I have decided to take `tidymodels` "for a spin", and create and execute a "tidy" modelling workflow to tackle a __classification problem__. My aim is to show how easy it is to fit a simple ___logistic regression___ in R's `glm` and quickly switch to a ___cross-validated random forest___ using the `ranger` engine by changing only a few lines of code.

For this post in particular I'm focusing on four different libraries from the `tidymodels` suite:

- `parsnip` for machine learning and modeling
- `rsample` for data sampling and 10-fold cross-validation
- `recipes` for data preprocessing
- `yardstick` for model assessment.

Note that the focus is on modelling workflow and libraries interaction. For that reason, I am keeping data exploration and feature engineering to a minimum. __Data exploration, data wrangling, visualization, and business understanding are _CRITICAL_ to your ability to perform machine learning__. If you want to learn the end-to-end process for completing business projects with data science with `H2O` and `parsnip` and `Shiny` web applications using `AWS`, then I recommend Business Science's [4-Course R-Track System](https://university.business-science.io/p/4-course-bundle-machine-learning-and-web-applications-r-track-101-102-201-202a/) - One complete system to go from beginner to expert in 6-months.

{% include cta_rtrack.html %}

# My Workflow {#workflow}

Here's a diagram of the workflow I used to web scrape the Specialized Data and create an application:

1. Start with raw data in CSV format

2. Use `skimr` to _quickly_ understand the features

3. Use `rsample` to split into training/testing sets

4. Use `recipes` to create data preprocessing pipeline

4. Use `parsnip`, `rsample` and `yardstick` to build models and assess machine learning performance

  My Code Workflow for Machine Learning with parsnip

# Tutorial - Churn Classification using Machine Learning

This is an ___intermediate tutorial to expose business analysts and data scientists to churn modeling___ with the new `parsnip` Machine Learning API.

## 1.0 Setup and Data

First, I load the packages I need for this analysis.

{% highlight r %}
library(tidyverse)   # Loads dplyr, ggplot2, purrr, and other useful packages
library(tidymodels)  # Loads parsnip, rsample, recipes, yardstick
library(skimr)       # Quickly get a sense of data
library(knitr)       # Pretty HTML Tables
{% endhighlight %}

For this project I am using the [Telco Customer Churn](https://www.ibm.com/communities/analytics/watson-analytics-blog/predictive-insights-in-the-telco-customer-churn-data-set/) from _IBM Watson Analytics_, one of IBM Analytics Communities. The data contains 7,043 rows, each representing a customer, and 21 columns for the potential predictors, providing information to forecast customer behaviour and help develop focused customer retention programmes.

__Churn__ is the ___Dependent Variable___ and shows the _customers who left within the last month_. The dataset also includes details on the __Services__ that each customer has signed up for, along with __Customer Account__ and __Demographic__ information.

Next, we read in the data (I have hosted on my [GitHub repo for this project](https://github.com/DiegoUsaiUK/Classification_Churn_with_Parsnip)).

{% highlight r %}
telco <- read_csv("https://raw.githubusercontent.com/DiegoUsaiUK/Classification_Churn_with_Parsnip/master/00_Data/WA_Fn-UseC_-Telco-Customer-Churn.csv")

telco %>% head() %>% kable()
{% endhighlight %}

|customerID |gender | SeniorCitizen|Partner |Dependents | tenure|PhoneService |MultipleLines    |InternetService |OnlineSecurity |OnlineBackup |DeviceProtection |TechSupport |StreamingTV |StreamingMovies |Contract       |PaperlessBilling |PaymentMethod             | MonthlyCharges| TotalCharges|Churn |
|:----------|:------|-------------:|:-------|:----------|------:|:------------|:----------------|:---------------|:--------------|:------------|:----------------|:-----------|:-----------|:---------------|:--------------|:----------------|:-------------------------|--------------:|------------:|:-----|
|7590-VHVEG |Female |             0|Yes     |No         |      1|No           |No phone service |DSL             |No             |Yes          |No               |No          |No          |No              |Month-to-month |Yes              |Electronic check          |          29.85|        29.85|No    |
|5575-GNVDE |Male   |             0|No      |No         |     34|Yes          |No               |DSL             |Yes            |No           |Yes              |No          |No          |No              |One year       |No               |Mailed check              |          56.95|      1889.50|No    |
|3668-QPYBK |Male   |             0|No      |No         |      2|Yes          |No               |DSL             |Yes            |Yes          |No               |No          |No          |No              |Month-to-month |Yes              |Mailed check              |          53.85|       108.15|Yes   |
|7795-CFOCW |Male   |             0|No      |No         |     45|No           |No phone service |DSL             |Yes            |No           |Yes              |Yes         |No          |No              |One year       |No               |Bank transfer (automatic) |          42.30|      1840.75|No    |
|9237-HQITU |Female |             0|No      |No         |      2|Yes          |No               |Fiber optic     |No             |No           |No               |No          |No          |No              |Month-to-month |Yes              |Electronic check          |          70.70|       151.65|Yes   |
|9305-CDSKC |Female |             0|No      |No         |      8|Yes          |Yes              |Fiber optic     |No             |No           |Yes              |No          |Yes         |Yes             |Month-to-month |Yes              |Electronic check          |          99.65|       820.50|Yes   |

## 2.0 Skim the Data

We can get a quick sense of the data using the `skim()` function from the `skimr` package.

{% highlight r %}
telco %>% skim()
{% endhighlight %}

{% highlight text %}
## Skim summary statistics
##  n obs: 7043
##  n variables: 21
##
## ── Variable type:character ───────────────────────────────────────────────────────────────────────────────────────────────────
##          variable missing complete    n min max empty n_unique
##             Churn       0     7043 7043   2   3     0        2
##          Contract       0     7043 7043   8  14     0        3
##        customerID       0     7043 7043  10  10     0     7043
##        Dependents       0     7043 7043   2   3     0        2
##  DeviceProtection       0     7043 7043   2  19     0        3
##            gender       0     7043 7043   4   6     0        2
##   InternetService       0     7043 7043   2  11     0        3
##     MultipleLines       0     7043 7043   2  16     0        3
##      OnlineBackup       0     7043 7043   2  19     0        3
##    OnlineSecurity       0     7043 7043   2  19     0        3
##  PaperlessBilling       0     7043 7043   2   3     0        2
##           Partner       0     7043 7043   2   3     0        2
##     PaymentMethod       0     7043 7043  12  25     0        4
##      PhoneService       0     7043 7043   2   3     0        2
##   StreamingMovies       0     7043 7043   2  19     0        3
##       StreamingTV       0     7043 7043   2  19     0        3
##       TechSupport       0     7043 7043   2  19     0        3
##
## ── Variable type:numeric ─────────────────────────────────────────────────────────────────────────────────────────────────────
##        variable missing complete    n    mean      sd    p0    p25
##  MonthlyCharges       0     7043 7043   64.76   30.09 18.25  35.5
##   SeniorCitizen       0     7043 7043    0.16    0.37  0      0
##          tenure       0     7043 7043   32.37   24.56  0      9
##    TotalCharges      11     7032 7043 2283.3  2266.77 18.8  401.45
##      p50     p75    p100     hist
##    70.35   89.85  118.75 ▇▁▃▂▆▅▅▂
##     0       0       1    ▇▁▁▁▁▁▁▂
##    29      55      72    ▇▃▃▂▂▃▃▅
##  1397.47 3794.74 8684.8  ▇▃▂▂▁▁▁▁
{% endhighlight %}

There are a couple of things to notice here:

- __customerID__ is a unique identifier for each row. As such it has no descriptive or predictive power and it needs to be removed.

- Given the relative small number of missing values in __TotalCharges__ (only 11 of them) I am dropping them from the dataset.

{% highlight r %}
telco <- telco %>%
    select(-customerID) %>%
    drop_na()
{% endhighlight %}

## 3.0 Tidymodels Workflow - Generalized Linear Model (Baseline)

To show the basic steps in the `tidymodels` framework I am fitting and evaluating a simple logistic regression model as a baseline.

### 3.1 Train/Test Split

`rsample` provides a streamlined way to create a randomised training and test split of the original data.

{% highlight r %}
set.seed(seed = 1972)

train_test_split <-
    rsample::initial_split(
        data = telco,
        prop = 0.80
    )

train_test_split
{% endhighlight %}

{% highlight text %}
## <5626/1406/7032>
{% endhighlight %}

Of the 7,043 total customers, 5,626 have been assigned to the training set and 1,406 to the test set. I save them as `train_tbl` and `test_tbl`.

{% highlight r %}
train_tbl <- train_test_split %>% training()
test_tbl  <- train_test_split %>% testing()
{% endhighlight %}

### 3.2 Prepare

The `recipes` package uses a __cooking metaphor__ to handle all the data preprocessing, like missing values imputation, removing predictors, centring and scaling, one-hot-encoding, and more.

First, I create a `recipe` where I define the transformations I want to apply to my data. In this case I create a simple recipe to change all character variables to factors.

Then, I _"prep the recipe"_ by mixing the ingredients with prep. Here I have included the prep bit in the recipe function for brevity.

{% highlight r %}
recipe_simple <- function(dataset) {
    recipe(Churn ~ ., data = dataset) %>%
        step_string2factor(all_nominal(), -all_outcomes()) %>%
        prep(data = dataset)
}
{% endhighlight %}

__Note__ - In order to avoid ___Data Leakage___ (e.g: transferring information from the train set into the test set), data should be "prepped" using the train_tbl only.

{% highlight r %}
recipe_prepped <- recipe_simple(dataset = train_tbl)
{% endhighlight %}

Finally, to continue with the cooking metaphor, I _"bake the recipe"_ to apply all preprocessing to the data sets.

{% highlight r %}
train_baked <- bake(recipe_prepped, new_data = train_tbl)
test_baked  <- bake(recipe_prepped, new_data = test_tbl)
{% endhighlight %}

### 3.3 Machine Learning and Performance

#### Fit the Model

`parsnip` is a recent addition to the `tidymodel`s suite and is probably the one I like best. This package offers a unified API that allows access to several machine learning packages without the need to learn the syntax of each individual one.

With 3 simple steps you can:

1. Set the type of model you want to fit (here is a logistic regression) and its mode (classification)

2. Decide which computational engine to use (glm in this case)

3. Spell out the exact model specification to fit (I'm using all variables here) and what data to use (the baked train dataset)

{% highlight r %}
logistic_glm <- logistic_reg(mode = "classification") %>%
    set_engine("glm") %>%
    fit(Churn ~ ., data = train_baked)
{% endhighlight %}

If you want to use another engine, you can simply switch the `set_engine` argument (for logistic regression you can choose from `glm`, `glmnet`, `stan`, `spark`, and `keras`) and `parsnip` will take care of changing everything else for you behind the scenes.

#### Assess Performance

{% highlight r %}
predictions_glm <- logistic_glm %>%
    predict(new_data = test_baked) %>%
    bind_cols(test_baked %>% select(Churn))

predictions_glm %>% head() %>% kable()
{% endhighlight %}

|.pred_class |Churn |
|:-----------|:-----|
|Yes         |No    |
|No          |No    |
|No          |No    |
|No          |No    |
|No          |No    |
|No          |No    |

There are several metrics that can be used to investigate the performance of a classification model but for simplicity I'm only focusing on a selection of them: ___accuracy, precision, recall and F1_Score___.

All of these measures (and many more) can be derived by the ___Confusion Matrix___, a table used to describe the performance of a classification model on a set of test data for which the true values are known.

In and of itself, the confusion matrix is a relatively easy concept to get your head around as is shows the number of _false positives_, _false negatives_, _true positives_, and _true negatives_. However some of the measures that are derived from it may take some reasoning with to fully understand their meaning and use.

{% highlight r %}
predictions_glm %>%
    conf_mat(Churn, .pred_class) %>%
    pluck(1) %>%
    as_tibble() %>%

    # Visualize with ggplot
    ggplot(aes(Prediction, Truth, alpha = n)) +
    geom_tile(show.legend = FALSE) +
    geom_text(aes(label = n), colour = "white", alpha = 1, size = 8)
{% endhighlight %}

![plot of chunk unnamed-chunk-12](/figure/source/2019-11-18-parsnip-churn-classification-machine-learning/unnamed-chunk-12-1.png)

##### Accuracy

The model's ___Accuracy___ is the fraction of predictions the model got right and can be easily calculated by passing the predictions_glm to the metrics function. However, accuracy is not a very reliable metric as it will provide misleading results if the data set is unbalanced.

With only basic data manipulation and feature engineering the simple logistic model has achieved 80% accuracy.

{% highlight r %}
predictions_glm %>%
    metrics(Churn, .pred_class) %>%
    select(-.estimator) %>%
    filter(.metric == "accuracy") %>%
    kable()
{% endhighlight %}

|.metric  | .estimate|
|:--------|---------:|
|accuracy | 0.8058321|

##### Precision and Recall

___Precision___ shows how sensitive models are to False Positives (i.e. predicting a customer is leaving when he-she is actually staying) whereas ___Recall___ looks at how sensitive models are to False Negatives (i.e. forecasting that a customer is staying whilst he-she is in fact leaving).

__These are very relevant business metrics__ because organisations are particularly interested in accurately predicting which customers are truly at risk of leaving so that they can target them with retention strategies. At the same time they want to minimising efforts of retaining customers incorrectly classified as leaving who are instead staying.

{% highlight r %}
tibble(
    "precision" =
        precision(predictions_glm, Churn, .pred_class) %>%
        select(.estimate),
    "recall" =
        recall(predictions_glm, Churn, .pred_class) %>%
        select(.estimate)
) %>%
    unnest(cols = c(precision, recall)) %>%
    kable()
{% endhighlight %}

| precision|    recall|
|---------:|---------:|
| 0.8466368| 0.9024857|

##### F1 Score

Another popular performance assessment metric is the ___F1 Score___, which is the harmonic average of the precision and recall. An F1 score reaches its best value at 1 with perfect precision and recall.

{% highlight r %}
predictions_glm %>%
    f_meas(Churn, .pred_class) %>%
    select(-.estimator) %>%
    kable()
{% endhighlight %}

|.metric | .estimate|
|:-------|---------:|
|f_meas  | 0.8736696|

## 4.0 Random Forest - Machine Learning Modeling and Cross Validation

This is where the real beauty of `tidymodels` comes into play. Now I can use this tidy modelling framework to fit a __Random Forest__ model with the `ranger` engine.

### 4.1 Cross Validation - 10-Fold

To further refine the model's predictive power, I am implementing a __10-fold cross validation__ using `vfold_cv` from `rsample`, which splits again the initial training data.

{% highlight r %}
cross_val_tbl <- vfold_cv(train_tbl, v = 10)
cross_val_tbl
{% endhighlight %}

{% highlight text %}
## #  10-fold cross-validation
## # A tibble: 10 x 2
##    splits             id
##    <named list>       <chr>
##  1 <split [5.1K/563]> Fold01
##  2 <split [5.1K/563]> Fold02
##  3 <split [5.1K/563]> Fold03
##  4 <split [5.1K/563]> Fold04
##  5 <split [5.1K/563]> Fold05
##  6 <split [5.1K/563]> Fold06
##  7 <split [5.1K/562]> Fold07
##  8 <split [5.1K/562]> Fold08
##  9 <split [5.1K/562]> Fold09
## 10 <split [5.1K/562]> Fold10
{% endhighlight %}

If we take a further look, we should recognise the 5,626 number, which is the total number of observations in the initial train_tbl. In each round, 563 observations will in turn be retained from estimation and used to validate the model for that fold.

{% highlight r %}
cross_val_tbl %>% pluck("splits", 1)
{% endhighlight %}

{% highlight text %}
## <5063/563/5626>
{% endhighlight %}

To avoid confusion and distinguish the initial train/test splits from those used for cross validation, the author of rsample Max Kuhn has coined two new terms: the ___analysis___ and the ___assessment____ sets. The former is the portion of the train data used to recursively estimate the model, where the latter is the portion used to validate each estimate.

### 4.2 Machine Learning

#### Random Forest

Switching to another model could not be simpler! All I need to do is to change the __type of model__ to `random_forest`, add its hyper-parameters, change the `set_engine` argument to `ranger`, and I'm ready to go.

I'm bundling all steps into a function that estimates the model across all folds, runs predictions and returns a convenient tibble with all the results. I need to add an extra step before the recipe "prepping" to maps the cross validation splits to the `analysis()` and `assessment()` functions. This will guide the iterations through the 10 folds.

{% highlight r %}
rf_fun <- function(split, id, try, tree) {

    analysis_set <- split %>% analysis()
    analysis_prepped <- analysis_set %>% recipe_simple()
    analysis_baked <- analysis_prepped %>% bake(new_data = analysis_set)

    model_rf <-
        rand_forest(
            mode = "classification",
            mtry = try,
            trees = tree
        ) %>%
        set_engine("ranger",
                   importance = "impurity"
        ) %>%
        fit(Churn ~ ., data = analysis_baked)

    assessment_set     <- split %>% assessment()
    assessment_prepped <- assessment_set %>% recipe_simple()
    assessment_baked   <- assessment_prepped %>% bake(new_data = assessment_set)

    tibble(
        "id" = id,
        "truth" = assessment_baked$Churn,
        "prediction" = model_rf %>%
            predict(new_data = assessment_baked) %>%
            unlist()
    )

}
{% endhighlight %}

#### Modeling with purrr

I iteratively apply the random forest modeling function, `rf_fun()`, to each of the 10 cross validation folds using `purrr`.

{% highlight r %}
pred_rf <- map2_df(
    .x = cross_val_tbl$splits,
    .y = cross_val_tbl$id,
    ~ rf_fun(split = .x, id = .y, try = 3, tree = 200)
)

head(pred_rf)
{% endhighlight %}

{% highlight text %}
## # A tibble: 6 x 3
##   id     truth prediction
##   <chr>  <fct> <fct>
## 1 Fold01 Yes   Yes
## 2 Fold01 Yes   No
## 3 Fold01 Yes   Yes
## 4 Fold01 No    No
## 5 Fold01 No    No
## 6 Fold01 Yes   Yes
{% endhighlight %}

#### Assess Performance

I've found that `yardstick` has a very handy confusion matrix `summary()` function, which returns an array of __13 different confusion matrix metrics__ but in this case I want to see the four I used for the glm model.

{% highlight r %}
pred_rf %>%
    conf_mat(truth, prediction) %>%
    summary() %>%
    select(-.estimator) %>%
    filter(.metric %in% c("accuracy", "precision", "recall", "f_meas")) %>%
    kable()
{% endhighlight %}

|.metric   | .estimate|
|:---------|---------:|
|accuracy  | 0.7975471|
|precision | 0.8328118|
|recall    | 0.9050279|
|f_meas    | 0.8674194|

The `random forest` model is performing in par with the `simple logistic regression`. Given the very basic feature engineering that I've carried out, there is scope to further improve the model but this is beyond the scope of this post.

# Parting Thoughts

One of the great advantage of `tidymodels` is the flexibility and ease of access to every phase of the analysis workflow. Creating the modelling pipeline is a breeze and you can easily re-use the initial framework by changing model type with `parsnip` and data pre-processing with `recipes` and in no time you're ready to check your new model's performance with `yardstick`.

In any analysis you would typically audit several models and `parsnip` frees you up from having to learn the unique syntax of every modelling engine so that you can focus on finding the best solution for the problem at hand.

If you would like to learn how to ___apply Data Science to Business Problems___, take the program that I chose to build my skills. You will learn tools like `parsnip` and `H2O` for machine learning and `Shiny` for web applications, and many more critical tools (`tidyverse`, `recipes`, and more!) for applying data science to business problems. For a limited time you can get 15% OFF the [4-Course R-Track System](https://university.business-science.io/p/4-course-bundle-machine-learning-and-web-applications-r-track-101-102-201-202a/?coupon_code=ds4b15).

{% include cta_rtrack.html %}

# Code Repository

The full R code can be found on [my GitHub profile](https://github.com/DiegoUsaiUK/Classification_Churn_with_Parsnip).

{% include success_stories.md %}


text:
<mat-card>
  <mat-card-title>New Password</mat-card-title>
  <mat-list>
    <mat-list-item>
      <mat-form-field>
        <input matInput #password type="password" placeholder="Password" (keyup)="isPasswordValid(password.value)">
      </mat-form-field>

    </mat-list-item>
    <mat-list-item>
      <mat-form-field>
        <input matInput #confirmation type="password" placeholder="Confirm Password" (keyup)="isPasswordMatch(password.value, confirmation.value)"
          (keyup.enter)="onSubmitClick(password.value, confirmation.value)">
      </mat-form-field>

    </mat-list-item>
  </mat-list>
  <mat-card-actions>
    <button mat-raised-button [disabled]="!passwordMatch" (click)="onSubmitClick(password.value, confirmation.value)">Submit</button>
    <button mat-raised-button (click)="onCancelClick()">Cancel</button>
  </mat-card-actions>
</mat-card>


text:
package com.coreoz.plume.db.crud;

import java.util.List;
import java.util.Optional;

/**
 * A generic service that expose {@link CrudDao} operations.
 *
 * The common use is to extend this service.
 */
public class CrudService<T> {

	private final CrudDao<T> crudDao;

	public CrudService(CrudDao<T> crudDao) {
		this.crudDao = crudDao;
	}

	public List<T> findAll() {
		return crudDao.findAll();
	}

	public T findById(Long id) {
		if(id == null) {
			return null;
		}
		return crudDao.findById(id);
	}

	public Optional<T> findByIdOptional(Long id) {
		return Optional.ofNullable(crudDao.findById(id));
	}

	public T save(T entityToSave) {
		return crudDao.save(entityToSave);
	}

	public void delete(Long id) {
		crudDao.delete(id);
	}

}