<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Emre Havan on Medium]]></title>
        <description><![CDATA[Stories by Emre Havan on Medium]]></description>
        <link>https://medium.com/@emrehavan?source=rss-f1991f635894------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*qBJqFv7NHYC6O0ydiW99ag.jpeg</url>
            <title>Stories by Emre Havan on Medium</title>
            <link>https://medium.com/@emrehavan?source=rss-f1991f635894------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 24 Apr 2026 03:33:08 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@emrehavan/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Building a Scalable Apple Health Authorization Management View for iOS]]></title>
            <link>https://medium.com/fit-records/building-a-scalable-apple-health-authorization-management-view-for-ios-54012e34318a?source=rss-f1991f635894------2</link>
            <guid isPermaLink="false">https://medium.com/p/54012e34318a</guid>
            <category><![CDATA[healthkit]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[swiftui]]></category>
            <dc:creator><![CDATA[Emre Havan]]></dc:creator>
            <pubDate>Tue, 09 Jan 2024 08:20:36 GMT</pubDate>
            <atom:updated>2024-01-09T08:20:36.870Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RT_kit2CobUjlrJ6nXrKdw.jpeg" /></figure><p>It usually seems pretty easy to ask for authorization for Apple Health from our iOS apps, we just need to call the requestAuthorization method, and if the user has not yet authorized or made a decision, they will see the authorization sheet, and if they had, they will see nothing. Well yeah, it’s pretty easy, but building a better experience for users, where they can see, in detail, whether they have authorized, or what data types they have authorized, or if there are new data types the app needs additional permissions for, can be a challenging and complicated task.</p><p>In this piece, we are going to build the same experience we have build for <a href="https://apps.apple.com/us/app/fit-records-workout-tracker/id6449899890">Fit Records</a>. Lets get started!</p><p><em>Disclaimer: In some parts of the code, we could have also used the new Swift Concurrency and also Dependency Injection for testability. But these are omitted for this article since it is focused on HealthKit Integration</em></p><h4>Getting To Know Data Types and APIs</h4><p>First, let’s identify the data types we are going to work with and take a look at the HealthKit APIs we are going to use.</p><p>In this example, we are going to request authorization for writing and reading data as follows:</p><p>Write Data Types: Active Calories Burned, Workouts</p><p>Read Data Types: Active Calories Burned, Heart Rate</p><p><strong>Now the HealthKit APIs we will use</strong>:</p><ul><li><a href="https://developer.apple.com/documentation/healthkit/hkhealthstore/1614152-requestauthorization">requestAuthorization(toShare:read:completion:)</a>: A method to request authorization for given write and read types, if user has not seen the permission view for the given types, the system presents the permission view, otherwise it calls the completion of the method directly.</li><li><a href="https://developer.apple.com/documentation/healthkit/hkhealthstore/2994346-getrequeststatusforauthorization">getRequestStatusForAuthorization(toShare:read:completion:)</a>: A method to understand whether the user has seen the permission view for the given write and read types. This will help us to show an authorize additional permissions button when we introduce new types in the future.</li><li><a href="https://developer.apple.com/documentation/healthkit/hkhealthstore/1614154-authorizationstatus">authorizationStatus(for:)</a>: A method to check the authorization status for the given health type. <strong>IMPORTANT! </strong>It is not possible to figure out whether user has granted permission for reading a certain data type, it only works for checking the status for writing a certain data type.</li></ul><p>Now that we’ve reviewed the data types and the important APIs, we can get started with building our view.</p><h3>Building the View</h3><p>We are going to build our view using SwiftUI, but before we get started with building the view, it is important to first define what we want to achieve, then build the viewModel (ObservableObject) which will be responsible for managing the view state for different HealthKit authorization states.</p><h4>What we want to achieve</h4><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgiphy.com%2Fembed%2FseGbBi6ffg8oSXObmF%2Ftwitter%2Fiframe&amp;display_name=Giphy&amp;url=https%3A%2F%2Fmedia.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExZHdheDlqMW5hYnduM25uOGJndDZlamw4NHF2ZWQwcTJ3dWVjZG54cSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FseGbBi6ffg8oSXObmF%2Fgiphy.gif&amp;image=https%3A%2F%2Fmedia0.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExeDM1MHNxbDV6ZTB6bWxsY3psY2ptZ3ZiN3A3aDhncjN2dWF2aXFkbyZlcD12MV9naWZzX2dpZklkJmN0PWc%2FseGbBi6ffg8oSXObmF%2Fgiphy.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=giphy" width="435" height="435" frameborder="0" scrolling="no"><a href="https://medium.com/media/dc08c3f213f2e11c9acf296633aae7f5/href">https://medium.com/media/dc08c3f213f2e11c9acf296633aae7f5/href</a></iframe><p>We want to provide an informative Apple Health Integration Management View for our users, and it can be best described with its different states.</p><ul><li><strong>User has not seen the authorization view</strong>: In this state, the user has not yet made a decision on authorizing any health data, we want to show them a button to trigger the authorization flow</li><li><strong>User has denied authorization for all data types: </strong>In this state, the user has denied providing access to any health data, we want to show that the Health is not integrated</li><li><strong>User has denied authorization for all write data types, but authorized for some read data types: </strong>Since there is no way for us to understand if the user has granted access for a read type, if the user has denied access for all write types, we will assume the Health is not integrated, and the UI will be the same as the state above</li><li><strong>User has granted access for all or some write data types: </strong>In this state, we want to show users that the integration is active and indicate the authorization status for each write data type</li><li><strong>User has granted access for all or some write data types, but the app needs additional permission for a new write data type: </strong>In this state, we want to show users that the integration is active, indicating the authorization status for determined data types, and show that new data types are available for additional access, and a button to reauthorize the Health.</li><li><strong>User has granted access for all or some write data types, but the app needs additional permission for a new read data type: </strong>The state is the same as above, but this time the app introduced a new read data type, since we cannot know if the user has granted permission for a read type, we only want to show a button to reauthorize the Health, without showing the status for this new read data type</li></ul><p>Phew, thats a lot of states, and we want to cover all of them in our UI.</p><p>We better get to work!</p><h4>Implementing the View Model</h4><p>We are going to implement an observable view model that will manage all the logic around different states. Before we get started with it though, let’s implement an enum to describe all the possible states we described above.</p><p>We are actually going to need two enums, one for describing the write data types we are interested in, and the second one to manage the state of the authorization status.</p><p>First, AppleHealthWriteDataTypes for describing the write data types we will use:</p><pre>enum AppleHealthWriteDataTypes: Hashable, Identifiable {<br>    var id: Self {<br>        return self<br>    }<br>    <br>    case activeCaloriesBurned(HKAuthorizationStatus)<br>    case workout(HKAuthorizationStatus)<br>}</pre><p>The cases also have an associated value of type <a href="https://developer.apple.com/documentation/healthkit/hkauthorizationstatus">HKAuthorizationStatus</a> it will be helpful for us to indicate whether a data type is authorized or not, later on when we draw the UI. Also, it conforms to Hashable and Identifiable so we can use it in a ForEach .</p><p>Now the main enum to manage all the state, HealthKitIntegrationState :</p><pre>enum HealthKitIntegrationState {<br>    case healthDataNotAvailable<br>    case loading<br>    case notDetermined<br>    case determined([AppleHealthWriteDataTypes])<br>    case partiallyDetermined(determinedWriteTypes: [AppleHealthWriteDataTypes], nonDeterminedWriteTypes: [AppleHealthWriteDataTypes])<br>}</pre><p>Lets go over them one by one:</p><ul><li>healthDataNotAvailable: HealthKit is not available for older iPads and Macbooks (partially), so depending on the user’s device, the health data may not be available. Learn more at <a href="https://developer.apple.com/documentation/healthkit/hkhealthstore/1614180-ishealthdataavailable">isHealthDataAvailable</a></li><li>loading: This is the state we will set initially, until we get more information about user’s apple health state</li><li>notDetermined: User has not made a decision yet</li><li>determined: The Apple Health state is determined</li><li>partiallyDetermined: User previously determined the state for data types, but the app introduced new data types that require additional determination</li></ul><h4>Some Helpers</h4><p>We are also going to implement some helper entities to get the read and write types we use, request authorization and check authorization status for given types.</p><p><strong>AppleHealthUsedDataTypeProvider</strong></p><p>A simple enum with two static functions to provide read and write data types for HealthKit</p><pre>enum AppleHealthUsedDataTypeProvider {<br>    static func provideReadTypes() -&gt; Set&lt;HKObjectType&gt;? {<br>        guard HKHealthStore.isHealthDataAvailable(),<br>              let activeCaloriesBurned = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),<br>              let heartRate = HKObjectType.quantityType(forIdentifier: .heartRate) else {<br>            return nil<br>        }<br>        return [activeCaloriesBurned, heartRate]<br>    }<br><br>    static func provideWriteTypes() -&gt; Set&lt;HKSampleType&gt;? {<br>        guard HKHealthStore.isHealthDataAvailable(),<br>              let activeCaloriesBurned = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned) else {<br>            return nil<br>        }<br>        return [activeCaloriesBurned, .workoutType()]<br>    }<br>}</pre><p><strong>AppleHealthAuthorisationRequester</strong></p><p>Another simple enum with two methods to request authorization and get the authorization status from HealthKit</p><pre>enum AppleHealthAuthorisationRequester {<br>    static func requestAuthorisation(onCompletion: @escaping () -&gt; Void) {<br>        guard let writeDataTypes = AppleHealthUsedDataTypeProvider.provideWriteTypes(),<br>              let readDataTypes = AppleHealthUsedDataTypeProvider.provideReadTypes() else {<br>            DispatchQueue.main.async {<br>                onCompletion()<br>            }<br>            return<br>        }<br><br>        HKHealthStore().requestAuthorization(toShare: writeDataTypes, read: readDataTypes) { success, error in<br>            DispatchQueue.main.async {<br>                onCompletion()<br>            }<br>        }<br>    }<br><br>    static func requestStatusForAuthorisation(onCompletion: @escaping (HKAuthorizationRequestStatus) -&gt; Void) {<br>        guard let writeDataTypes = AppleHealthUsedDataTypeProvider.provideWriteTypes(),<br>              let readDataTypes = AppleHealthUsedDataTypeProvider.provideReadTypes() else {<br>            DispatchQueue.main.async {<br>                onCompletion(.unknown)<br>            }<br>            return<br>        }<br><br>        HKHealthStore().getRequestStatusForAuthorization(toShare: writeDataTypes, read: readDataTypes) { status, error in<br>            DispatchQueue.main.async {<br>                onCompletion(status)<br>            }<br>        }<br>    }<br>}</pre><p>Great, now we can finally get started with implementing the view model! Enter, <strong>AppleHealthIntegrationViewModel</strong></p><pre>final class AppleHealthIntegrationViewModel: ObservableObject {<br>    @Published var state: HealthKitIntegrationState = .loading<br><br>    init() {<br>        getAuthorisationStatusForAppleHealthDataTypes()<br>    }<br><br>    private func getAuthorisationStatusForAppleHealthDataTypes() {<br>        // Will be implemented soon<br>    }<br>}</pre><p>For now its a simple view model with a published state property set to .loading at first, and a method called right after initialisation to get the authorization status.</p><h4>Computing the HealthKit Authorization State</h4><p>Now we are going to implement the body of getAuthorisationStatusForAppleHealthDataTypes to check the authorization status and update our state properly, with the needed associated values, but before doing that, we need to add a new property to AppleHealthWriteDataTypes we implemented earlier, to make our lives easier later on:</p><pre>extension AppleHealthWriteDataTypes {<br>    var isDetermined: Bool {<br>        switch self {<br>        case .activeCaloriesBurned(let authStatus),<br>                .workout(let authStatus):<br>            return authStatus != .notDetermined<br>        }<br>    }<br>}</pre><p>isDetermined will be used to identify the determined types easily.</p><p>Okay now we can get back to implementing getAuthorisationStatusForAppleHealthDataTypes in the view model:</p><pre>private func getAuthorisationStatusForAppleHealthDataTypes() {<br>    guard let activeCaloriesBurned = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned) else {<br>        state = .notDetermined<br>        return<br>    }<br><br>    let healthStore = HKHealthStore()<br><br>    // 2<br>    // Workout Write Type<br>    let workoutAuthorisationStatus = healthStore.authorizationStatus(for: .workoutType())<br>    let workoutType: AppleHealthWriteDataTypes = .workout(workoutAuthorisationStatus)<br><br>    // Active Calories Write Type<br>    let activeCaloriesBurnedAuthorisationStatus = healthStore.authorizationStatus(for: activeCaloriesBurned)<br>    let activeCaloriesBurnedType: AppleHealthWriteDataTypes = .activeCaloriesBurned(activeCaloriesBurnedAuthorisationStatus)<br>    <br>    // 3<br>    let appleHealthWriteTypes = [workoutType, activeCaloriesBurnedType]<br>    let determinedWriteHealthTypes = appleHealthWriteTypes.filter { $0.isDetermined }<br>    let nonDeterminedWriteHealthTypes = appleHealthWriteTypes.filter { $0.isDetermined == false }<br><br>    // 4<br>    AppleHealthAuthorisationRequester.requestStatusForAuthorisation { status in<br>        self.updateState(<br>            requestStatusForAuthorisation: status,<br>            nonDeterminedWriteHealthTypes: nonDeterminedWriteHealthTypes,<br>            determinedWriteHealthTypes: determinedWriteHealthTypes,<br>            appleHealthWriteTypes: appleHealthWriteTypes<br>        )<br>    }<br>}<br><br>private func updateState(<br>    requestStatusForAuthorisation: HKAuthorizationRequestStatus,<br>    nonDeterminedWriteHealthTypes: [AppleHealthWriteDataTypes],<br>    determinedWriteHealthTypes: [AppleHealthWriteDataTypes],<br>    appleHealthWriteTypes: [AppleHealthWriteDataTypes]<br>) {<br>    // 5<br>    switch requestStatusForAuthorisation {<br>    case .unknown:<br>        // 6<br>        state = .healthDataNotAvailable<br>    case .unnecessary:<br>        // 7<br>        state = .determined(determinedWriteHealthTypes)<br>    case .shouldRequest:<br>        // 8<br>        if determinedWriteHealthTypes.count == 0 {<br>            state = .notDetermined<br>        } else {<br>            state = .partiallyDetermined(determinedWriteTypes: determinedWriteHealthTypes, nonDeterminedWriteTypes: nonDeterminedWriteHealthTypes)<br>        }<br>    }<br>}</pre><p>Lets see what we do now step by step as indicated by the numbered comments:</p><ul><li>1) First we declare a HKQuantityType for the active calories burned, and declare a HKHealthStore</li><li>2) Then we get the authorization status for our write types, and then set our custom types for further processing. You might be wondering, why we need an additional enum, and why we don’t just work with HKSampleTypes provided with HealthKit? It’s because HKSampleType is not an enum to easily identify what kind of sample type that is. Thus, we map them to our own health data type, AppleHealthWriteDataTypes we created earlier.</li><li>3) Then we declare three different arrays, first, appleHealthWriteTypes that includes all the data types we want to write, then we declare determinedWriteHealthTypes, including only the types the user has already seen and determined its state (authorized or denied), by filtering the first array with recently created isDetermined value. Then the final one, nonDeterminedWriteHealthTypes, including the ones are not yet determined, meaning the user didn’t make a decision for those types yet.</li><li>4) Then we ask the request status for authorization with our helper AppleHealthAuthorisationRequester and pass the status of type <a href="https://developer.apple.com/documentation/healthkit/hkauthorizationrequeststatus">HKAuthorizationRequestStatus</a> to updateState method, along with all the arrays we have declared earlier.</li><li>5) In the updateState method, we switch on the requestStatusForAuthorisation</li><li>6) If the case is .unknown, that means an error occurred during the retrieval of the auth status, then we can set our view’s state to healthDataNotAvailable (You could alternatively implement a separate state for this to show a different error, but we used the same state for when the health data is not available on the device)</li><li>7) If the case is .unnecessary , that means for the given read and write types, the user has already made a decision, so we can set our state to .determined, with the determinedWriteTypes we have created earlier.</li><li>8) Finally for the case .shouldRequest, we need to do a bit more, to understand, if we will request authorization for the first time, or if we did request authorization in the past, but now we need it again because we introduced additional data types at a later version. We understand it by looking at the determinedWriteHealthTypes count, if its greater than 0, that means there were some write types determined earlier, meaning we need to request additional health types, so we set our state to .partiallyDetermined, by also passing the determinedWriteHealthTypes and nonDeterminedWriteHealthTypes. If the count is 0 though, we set our state to .notDetermined</li></ul><p>With this logic in place, we can now cover all cases we wanted to achieve. <strong>Please note that</strong>, since there is no way to identify if a data type for reading values from is authorized, if your app is only concerned with reading data from HealthKit, this approach won’t work. Similarly, also for Fit Records, if the user only enables reading data but disables writing data to HealthKit, the UI will look as if the user has denied all access. This is something we are fine with, given that there is no additional API to verify the status for read types, and our app really needs the write authorization to properly implement HealthKit integration :)</p><p>Before we move onto the view, we will implement one more thing for our view model, as you will see in the view examples in the next section, user can jump to the Health App to do modifications, and come back to our app. Since we show detailed write type authorization status, we need to make sure our view won’t show an outdated state when user comes back, in case they make any changes.</p><p>We will achieve this by observing the willEnterForeground notification and recomputing our state as follows:</p><p>Inside the init we will add the view model as an observer for the UIApplication.willEnterForegroundNotification notification:</p><pre>NotificationCenter.default.addObserver(<br>    self,<br>    selector: #selector(appWillEnterForeground),<br>    name: UIApplication.willEnterForegroundNotification,<br>    object: nil<br>)</pre><p>Then we will also implement the appWillEnterForeground method where we trigger getting authorization status:</p><pre>@objc<br>private func appWillEnterForeground() {<br>    getAuthorisationStatusForWriteTypes()<br>}</pre><p>Thats it, now if user jumps to Health App and comes back to Fit Records, we will show the most up to date HealthKit integration state :)</p><h4>Implementing the View</h4><p>For brevity, we are not going to go into details of all the subviews, but we will briefly discuss their implementation details. Drop a comment if you would like another article showing the implementation details of the subviews though :)</p><p>Introducing <strong>AppleHealthIntegrationView</strong></p><pre>struct AppleHealthIntegrationView: View {<br>    <br>    @StateObject var viewModel: AppleHealthIntegrationViewModel<br>    <br>    var body: some View {<br>        VStack {<br>            VStack {<br>                switch viewModel.state {<br>                case .healthDataNotAvailable:<br>                    makeHealthDataNotAvailableView()<br>                case .loading:<br>                    ProgressView()<br>                case .notDetermined:<br>                    makeNonDeterminedView()<br>                case .determined(let determinedTypes):<br>                    makeDeterminedView(determinedTypes: determinedTypes)<br>                case .partiallyDetermined(let determinedTypes, let nonDeterminedTypes):<br>                    makePartiallyDeterminedView(determinedTypes: determinedTypes, nonDeterminedTypes: nonDeterminedTypes)<br>                }<br>            }<br>            .padding()<br>            .background(Color(uiColor: .systemGray6).cornerRadius(16.0))<br>            .padding()<br>            Spacer()<br>        }<br>    }<br>}</pre><p>It is a simple view with one StateObject, the viewModel , then in its body, we switch on the viewModel’s state to draw our UI. Let’s discuss how and what we show for each state:</p><p><strong>makeHealthDataNotAvailableView()</strong></p><p>This view is shown when the health data is not available in user’s device, and it looks like the following:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/273/1*MRnuMkS5uZWeKoXOjcPZXQ.png" /></figure><p><strong>makeNonDeterminedView()</strong></p><p>This view is shown when the user has not interacted with Apple Health authorization yet, it shows some descriptive labels, and an Integrate button, triggering the authorization request to the system, by calling the previously implementedrequestAuthorisation method of AppleHealthAuthorisationRequester. It looks like the following:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/273/1*RaGX4fAk5_MeqEZwO8AjFA.png" /></figure><p><strong>makeDeterminedView(determinedTypes:)</strong></p><p>This method takes an array of determined write types and provides one of the two views. If at least one of the write types is authorized, it shows the authorization status as following by using a ForEach for all the provided determined write types (Shows checkmark if authorized, and an xmark if denied):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/273/1*bdZA2-4FSBbXmTTZWShuQQ.png" /></figure><p>But if the all the write types are denied, it shows the following view:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/273/1*GH21BKvq8epKSSLjFmN0cw.png" /></figure><p>In order to easily identify whether at least one write type is authorized, we first need to add another extension to AppleHealthWriteDataTypes :</p><pre>extension AppleHealthWriteDataTypes {<br>    var isAuthorised: Bool {<br>        switch self {<br>        case .activeCaloriesBurned(let authStatus),<br>                .workout(let authStatus):<br>            return authStatus == .sharingAuthorized<br>        }<br>    }<br>}</pre><p>And in AppleHealthIntegrationViewModel we need to add an internal method for the view to interact with, and a private method to check authorization status for each determined write type (The view could actually compute this with the data provided from viewModel’s state, but we kept these methods in the view model to keep the view as logic free as possible):</p><pre>func isAtLeastOneWriteTypeAuthorised() -&gt; Bool {<br>    switch state {<br>    case .healthDataNotAvailable, .loading, .notDetermined:<br>        assertionFailure(&quot;This method shouldn&#39;t have been called for a state other than determined or partially determined!&quot;)<br>        return false<br>    case .determined(let healthTypes):<br>        return isAtLeastOneTypeIsAuthorised(determinedTypes: healthTypes)<br>    case .partiallyDetermined(let determinedTypes, _):<br>        return isAtLeastOneTypeIsAuthorised(determinedTypes: determinedTypes)<br>    }<br>}<br><br>private func isAtLeastOneTypeIsAuthorised(determinedTypes: [AppleHealthWriteDataTypes]) -&gt; Bool {<br>    for determinedType in determinedTypes {<br>        if determinedType.isAuthorised {<br>            return true<br>        }<br>    }<br>    return false<br>}</pre><p><strong>makePartiallyDeterminedView(determinedTypes:, nonDeterminedTypes:)</strong></p><p>As the last possible subview, this one shows the authorization status for the determined write types, and in a sub section, it shows the types that are yet to be determined, it uses two separate ForEach’s for both determinedTypes<strong> </strong>and<strong> </strong>nonDeterminedTypes<strong>, </strong>and a button to trigger the reauthorization flow for the new data types, and it looks like the following (Imagine we are introducing Height as a new type to write data in a future version):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/273/1*vA9gg7QioFXmQzmtWHWqJg.png" /></figure><p>Thats it! We have seen and discussed the views for all the possible states of a user in our health kit auhorisation management view. Although we omitted the implementation details for the views, with the APIs and the view model provided, you should be able to build a similar view for your apps in no time :)</p><h3>Introducing a New Health Data Type at a Later Version</h3><p>We have implemented the management view, and its view model, and also added for support for showing a section for new data types, that are yet to be determined in a subsection, along with a “Authorize Additional Access” button, but with the current set up, how can we introduce a new type, how easy it is?</p><p>Let’s have a look.</p><p>Imagine we want to introduce the Height as a new data type to write to HealthKit, to do that, we only need to update a few places in our code.</p><p>First, we need to update AppleHealthWriteDataTypes to include a new case for the height, and also edit its extensions of isDetermined and isAuthorised</p><pre>enum AppleHealthWriteDataTypes: Hashable, Identifiable {<br>    ...<br>    case height(HKAuthorizationStatus)<br>}<br><br>extension AppleHealthWriteDataTypes {<br>    var isDetermined: Bool {<br>        ...<br>            .height(let authStatus):<br>            return authStatus != .notDetermined<br>        }<br>    }<br>}<br><br>extension AppleHealthWriteDataTypes {<br>    var isAuthorised: Bool {<br>        ...<br>            .height(let authStatus):<br>            return authStatus == .sharingAuthorized<br>        }<br>    }<br>}</pre><p>Then we need to update AppleHealthUsedDataTypeProvider to include height in the write types set:</p><pre>enum AppleHealthUsedDataTypeProvider {<br>    ...<br>    <br>    static func provideWriteTypes() -&gt; Set&lt;HKSampleType&gt;? {<br>        guard HKHealthStore.isHealthDataAvailable(),<br>              let activeCaloriesBurned = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),<br>                let height = HKObjectType.quantityType(forIdentifier: .height) else {<br>            return nil<br>        }<br>        return [activeCaloriesBurned, .workoutType(), height]<br>    }<br>}</pre><p>Then finally, in the getAuthorisationStatusForAppleHealthDataTypes method of AppleHealthIntegrationViewModel we include the new type as:</p><pre>private func getAuthorisationStatusForWriteTypes() {<br>    guard let activeCaloriesBurned = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),<br>          let height = HKObjectType.quantityType(forIdentifier: .height) else {<br>        state = .notDetermined<br>        return<br>    }<br><br>    ...<br>    <br>    // Height Write Type<br>    let heightStatus = healthStore.authorizationStatus(for: height)<br>    let heightType: AppleHealthWriteDataTypes = .height(heightStatus)<br>    <br>    let appleHealthWriteTypes = [workoutType, activeCaloriesBurnedType, heightType]<br><br>    ...<br>}</pre><p>Thats all! Now, if we run the app again for a user that has already authorized some write types, they will see the new type available same as the image shared for a partially determined view in the above section.</p><h3>Final Words</h3><p>In this piece, we have implemented a scalable HealthKit authorization state management logic, and also an accompanying view to inform users of their current state of HealthKit Integration, same as we did for Fit Records. We have made sure to show the authorization status for each write data type when determined, and added support for introducing new data types in the future, while also caring for the states where authorization is not determined, or denied.</p><p>I hope you found this article useful. Let me know what you think about it in the comments section. How are you managing Health Integration State? :)</p><p>Also if you are looking for a modern iOS App to track your workouts and exercises, give us a shot!</p><p>Until next time 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=54012e34318a" width="1" height="1" alt=""><hr><p><a href="https://medium.com/fit-records/building-a-scalable-apple-health-authorization-management-view-for-ios-54012e34318a">Building a Scalable Apple Health Authorization Management View for iOS</a> was originally published in <a href="https://medium.com/fit-records">Fit Records</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What you didn’t know about URLSessionConfiguration’s waitsForConnectivity]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://levelup.gitconnected.com/what-you-didnt-know-about-urlsessionconfiguration-s-waitsforconnectivity-6d718afa2ae3?source=rss-f1991f635894------2"><img src="https://cdn-images-1.medium.com/max/2600/1*a5UkRcFH80TATM2x8sAYUg.jpeg" width="3417"></a></p><p class="medium-feed-snippet">Configure your URLSessions the right way for waitsForConnectivity feature</p><p class="medium-feed-link"><a href="https://levelup.gitconnected.com/what-you-didnt-know-about-urlsessionconfiguration-s-waitsforconnectivity-6d718afa2ae3?source=rss-f1991f635894------2">Continue reading on Level Up Coding »</a></p></div>]]></description>
            <link>https://levelup.gitconnected.com/what-you-didnt-know-about-urlsessionconfiguration-s-waitsforconnectivity-6d718afa2ae3?source=rss-f1991f635894------2</link>
            <guid isPermaLink="false">https://medium.com/p/6d718afa2ae3</guid>
            <category><![CDATA[ios-development]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[swiftui]]></category>
            <category><![CDATA[swift-programming]]></category>
            <dc:creator><![CDATA[Emre Havan]]></dc:creator>
            <pubDate>Fri, 17 Nov 2023 12:25:38 GMT</pubDate>
            <atom:updated>2023-11-19T02:32:21.127Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Use PassthroughSubject the Right Way in Your APIs With Combine]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/better-programming/use-passthroughsubject-the-right-way-in-your-apis-with-combine-6e529fe955ed?source=rss-f1991f635894------2"><img src="https://cdn-images-1.medium.com/max/2600/1*4U0uKRtxiEVMJjFbEj5Zaw.png" width="3540"></a></p><p class="medium-feed-snippet">There you are, trying to refactor the usage of that nasty notification center or implementing a new API where the consumers can observe&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/better-programming/use-passthroughsubject-the-right-way-in-your-apis-with-combine-6e529fe955ed?source=rss-f1991f635894------2">Continue reading on Better Programming »</a></p></div>]]></description>
            <link>https://medium.com/better-programming/use-passthroughsubject-the-right-way-in-your-apis-with-combine-6e529fe955ed?source=rss-f1991f635894------2</link>
            <guid isPermaLink="false">https://medium.com/p/6e529fe955ed</guid>
            <category><![CDATA[combine-framework]]></category>
            <category><![CDATA[swift-programming]]></category>
            <category><![CDATA[ios-development]]></category>
            <category><![CDATA[notification-center]]></category>
            <dc:creator><![CDATA[Emre Havan]]></dc:creator>
            <pubDate>Thu, 26 Oct 2023 13:42:21 GMT</pubDate>
            <atom:updated>2023-10-26T13:42:21.684Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Writing a modern iOS Networking Library with Swift Concurrency]]></title>
            <link>https://medium.com/getir/writing-a-modern-ios-networking-library-with-swift-concurrency-bb1cdbf12725?source=rss-f1991f635894------2</link>
            <guid isPermaLink="false">https://medium.com/p/bb1cdbf12725</guid>
            <category><![CDATA[modularization]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <category><![CDATA[swift-programming]]></category>
            <category><![CDATA[tech]]></category>
            <category><![CDATA[swift-concurrency]]></category>
            <dc:creator><![CDATA[Emre Havan]]></dc:creator>
            <pubDate>Mon, 06 Mar 2023 09:09:34 GMT</pubDate>
            <atom:updated>2023-03-06T09:09:34.709Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8DGchrL3bjPeStt584ZrGQ.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@choys_?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Conny Schneider</a> on <a href="https://unsplash.com/photos/pREq0ns_p_E?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></figcaption></figure><p>In the ever-changing nature of software development, we often find ourselves in the need of rewriting some of our code and libraries. Getir is no exception, although it was working fine, we wanted to rewrite the networking library of our iOS Project.</p><p>Let’s take a look at the benefits of a library rewrite:</p><ul><li>Support the modernization of the codebase</li><li>Provide easy-to-use APIs</li><li>Leverage newer system APIs for better performance and reliability</li><li>Transfer the ownership of code to the new developers</li></ul><p>In addition to the benefits mentioned above, we also wanted to rewrite the Networking library in particular for the following reasons:</p><ul><li>Codable support</li><li>Async-await support for the new Swift concurrency</li><li>Mock support for UI tests</li><li>Flexible API for different needs of different parts of the project</li><li>Increasing the unit test coverage of our Networking stack</li></ul><p>In this article, we are going to talk about our adventure in this rewriting process, our approaches, the challenges faced, and the lessons learned along the way. Let’s get started! 🚀</p><h4>Model Structure</h4><p>In terms of the models used for the request and response types with the new networking, we wanted to use Decodable and Encodable, which provides us with easy-to-use decoding and encoding APIs.</p><h4>Request Structure</h4><p>We wanted to provide an easy way to describe the details of a request, such as an endpoint path, parameters, HTTP method, and any request-specific headers.</p><p>Thus, we’ve created the following protocol:</p><pre>public protocol NetworkRequestable {<br>  var baseURL: String { get }<br>  var method: HTTPMethod { get }<br>  var path: String: { get }<br>  var parameters: Encodable? { get }<br>  var headers: [String: String]? { get }<br>}</pre><p>Additionally, we provided the following extension, since not all requests need parameters or headers:</p><pre>extension NetworkRequestable {<br>  public var parameters: Encodable? {<br>    nil<br>  }<br>  <br>  public var headers: [String: String]? {<br>    nil<br>  }<br>}</pre><p>An example request struct would now look like this:</p><pre>struct SampleRequest: NetworkRequestable {<br>  var method: HTTPMethod = .post <br>  var baseURL = “https://my-base-url.com”<br>  var path = “my/login/path”<br>  var parameters: Encodable?<br>}</pre><p><em>HTTPMethod is a basic enum describing the method to use for the request, later on, implementation details are omitted for brevity.</em></p><p>The model used for parameters is now a simple Encodable that looks like the following:</p><pre>struct SampleRequestParameters: Encodable {<br>  let testProperty: String<br>  let secondTestProperty: String<br>}</pre><p>Finally the initialization of the request:</p><pre>let parameters = SampleRequestParameters(testProperty: “test”, secondTestProperty: “test2”)<br>let request = SampleRequest(parameters: parameters)</pre><p>That’s it. The request is ready to be fired with the networking. But now, we have another problem, the base URL of a service doesn’t change often, but here we are providing it in the request, which means we are going to provide it for another request as well, it is repetitive and unnecessary. Also would be very hard to change all over the place if needed.</p><p>As a solution, we introduced a new protocol to define the baseURL, which acts as a middleman between requests and NetworkRequestable. Now all requests that need to connect to the same baseURL can conform to it.</p><pre>protocol MyDomainSpecificRequest: NetworkRequestable {<br>  var baseURL = “https://my-base-url.com”<br>}</pre><p>Now the SampleRequest can simply conform to MyDomainSpecificRequest and leave the baseURL out:</p><pre>struct SampleRequest: MyDomainSpecificRequest {<br>  var method: HTTPMethod = .post<br>  var path = “my/login/path”<br>  var parameters: Encodable?<br>}</pre><p>You might be wondering, why not just inject the baseURL in the Networking API? Because we wanted a single instance of a Networking to communicate with different services, and also wanted to keep it as lean as possible. Additionally, with this approach, we can keep our base URLs dynamic, outside of the Networking package. We could provide the baseURL conditionally in MyDomainSpecificRequest for example:</p><pre>protocol MyDomainSpecificRequest: NetworkRequestable {<br><br>  var baseURL: String {<br>    switch ExampleState.environment {<br>    case .development: <br>      return “https://my-base-url-development.com”<br>    case .production: <br>      return “https://my-base-url.com”<br>    }<br>  }<br>}</pre><h4>Response Structure</h4><p>When we make a network request, we often expect a response, but additionally, at Getir, we provide certain metadata that is available with every response, let’s have a look at the response structure</p><pre>{<br>  &quot;expectedResponse&quot;: { // Request specific response<br>    &quot;testField&quot;: &quot;testValue&quot;,<br>    &quot;testFieldTwo&quot;: 2,<br>    ...<br>  },<br>  &quot;metadata&quot; : { // Metadata sent with every response<br>    &quot;additionalField&quot;: &quot;Test&quot;,<br>    ...<br>  }<br>}</pre><p>So we needed to implement a way to provide the expected response and also the additional metadata attached to it. Previously this was done with subclassing, every response type would subclass the base response where the metadata properties were declared. But with the new implementation, we wanted to keep our response models as value types and implemented a struct with a generic associated response type to achieve this</p><pre>public struct SuccessResponseWrapper&lt;T: Decodable&gt;: Decodable {<br>  public let metadata: ResponseMetadata <br>  public let expectedResponse: T<br>}</pre><p>So whenever a request succeeds the callers will receive a wrapper, which contains the expected response, and the metadata. The only requirement of the actual response type is that it should be a Decodable.</p><h3>Constructing the Networking</h3><p>Moving on from the request and response models, while constructing the Networking, we aimed to follow the single responsibility principle and enable effective testing by dividing the Networking into distinct layers.</p><p>Now that we talked about the request and response model structures, we can move forward with Networking implementation. While constructing the Networking, we wanted to create different layers, adhere to the single responsibility principle, and also quickly write tests for certain interactions.</p><p>To keep things concise, we will only discuss the four primary entities, even though the actual implementation has additional dependencies.</p><ul><li>RequestFactory</li><li>RequestExecutor</li><li>RequestAdapters</li><li>ResponseParser</li></ul><h4>RequestFactory</h4><p>The request factory is responsible for translating a NetworkRequestable into a URLRequest for the request execution. It has only one internal method and looks like the following:</p><pre>func makeURLRequest&lt;T: NetworkRequestable&gt;(with request: T) throws -&gt; URLRequest</pre><p>It does the following in the given order:</p><ul><li>Constructs the URL by combining the baseURL and the path</li><li>If there are parameters, encode them to Data with JSONEncoder</li><li>Depending on the HTTPMethod apply correct encoding (JSON or URL encoding)</li><li>Finally add the headers if they exist, and return the URLRequest</li></ul><p><strong>Encoding</strong></p><p>For the JSON encoding, it was trivial, we could just encode the Encodable parameters as Data and set it as the httpBody of the URLRequest . But when it comes to URL encoding, things were a bit more tricky.</p><p>The query parameters were created by using JSONSerialization(Force unwrapping used for example purposes):</p><pre>let queryParameters = try! JSONSerialization.jsonObject(<br>  with: parameters,<br>  options: .fragmentsAllowed<br>) as! [String: Any]<br><br>var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)!<br><br>urlComponents.queryItems = queryParameters.map {<br>  URLQueryItem(name: $0.key, value: String(describing: $0.value))<br>}</pre><p>Although it looked fine at the first sight, soon we realized a problem with this approach, it was sending query values for booleans as 1 and 0, instead of true and false. It was happening because <a href="https://forums.swift.org/t/jsonserialization-turns-bool-value-to-nsnumber/31909">JSONSerialization converts </a><a href="https://forums.swift.org/t/jsonserialization-turns-bool-value-to-nsnumber/31909">Bool to </a><a href="https://forums.swift.org/t/jsonserialization-turns-bool-value-to-nsnumber/31909">CFBoolean</a>, which acts as an Int when directly used with String(describing:).</p><p>To address the issue, we needed to check if the value was of type NSNumber when it is initialized with a boolean value. Additionally, we also needed to verify if the value could be converted to a Bool to ensure that we only convert actual boolean values and not mistakenly convert integer values to true or false.</p><p>Now the queryItems initialization looks like the following:</p><pre>urlComponents.queryItems = queryParameters.map {<br>  if type(of: $0.value) == type(of: NSNumber(value: true)),<br>    let value = $0.value as? Bool {<br>      return URLQueryItem(name: $0.key, value: &quot;\(value)&quot;<br>  }<br>  return URLQueryItem(name: $0.key, value: String(describing: $0.value))<br>}</pre><h4>RequestExecutor</h4><p>The request executor is responsible for executing the requests with the injected URLSession. It has one internal method:</p><pre>func execute(_ request: URLRequest) async -&gt; Result&lt;ExecutionSuccessModel, Error&gt; {<br>  do {<br>    let (data, response) = try await session.data(for: request)<br>    return .success(ExecutionSuccessModel(data: data, response: response))<br>  } catch let error {<br>    return .failure(error)<br>  }<br>}</pre><p>If the execution succeeds, it provides a ExecutionSuccessModel which consists of Data and URLResponse , and if it throws an error, it is returned back to the Networking.</p><h4>RequestAdapters</h4><p>Request adapters allow for final modifications of requests before they are executed. They are injected into Networking and applied sequentially after the URLRequest is created by the request factory.</p><p>All the adapters must conform to RequestAdaptation protocol, which only has one requirement:</p><pre>public protocol RequestAdaptation {<br>  func adapt(request: URLRequest) -&gt; URLRequest<br>}</pre><p>So an adapter can take a request, do something with it and provide it back.</p><p>Next we will take a look at an adapter that is used for providing the default headers for every ongoing request:</p><pre>public final class DefaultHTTPHeaderAdapter: RequestAdaptation {<br>  private var headerProvidingClosure: () -&gt; ([String: String])<br>  <br>  public init(headerProvidingClosure: @escaping () -&gt; ([String: String]) {<br>    self.headerProvidingClosure = headerProvidingClosure<br>  }<br><br>  public func adapt(request: URLRequest) -&gt; URLRequest {<br>    var mutableRequest = request<br>    let headers = headerProvidingClosure()<br>    headers.forEach {<br>      mutableRequest.setValue($0.value, forHTTPHeaderField: $0.key)<br>    }<br>    return mutableRequest<br>  }<br>}</pre><p>The DefaultHTTPHeaderAdapter is initialized with a headerProvidingClosure, which is a closure that takes nothing and provides headers as key-value pairs when needed. This can, later on, be injected into the Networking so every ongoing request can have the default headers applied.</p><h4>ResponseParser</h4><p>The response parser converts the retrieved data into the expected type + metadata for a request call. It returns the previously mentioned success model or a network error in case of any issues.</p><h4>Networking</h4><p>Now that we talked about all the important pieces, we can construct the Networking. In addition to the entities we talked about, the Networking also has its own URLSession. We are going to inject all our entities in the init method so that we can write unit tests easily later on:</p><pre>final public class Networking {<br>    private let requestMaker: RequestMaking<br>    private let requestExecutor: RequestExecuting<br>    private let requestAdapters: [RequestAdaptation]<br>    private let responseParser: ResponseParsing<br>    private let session: URLSession<br>    <br>    public init(requestMaker: /* all entities are injected */) {<br>        // properties are initialised<br>    }<br>}</pre><p>Here we realized another problem, the consumers of the library don’t need to know about all these internal entities. But to make the init public so it can be initialized outside, and we can continue using dependency injection, we have to make all our internal entity protocols public. Any consumer can implement these protocols and provide their own implementations, which is not what we want.</p><p><strong>Convenience init to the rescue<br></strong>To solve this issue, we created a public convenience initialiser and kept the original one internal, resulting in two separate init methods:</p><pre>public convenience init(requestAdapters: [RequestAdaptation] = []) {<br>    self.init(requestMaker: RequestFactory(),<br>              requestExecutor: RequestExecutor(),<br>              requestAdapters: requestAdapters,<br>              responseParser: ResponseParser())<br>}<br><br>init(requestMaker: RequestMaking, requestExecutor: RequestExecuting, requestAdapters: [RequestAdaptation], responseParser: ResponseParsing) {<br>    // properties initialised<br>}</pre><p>We made only the RequestAdaptation public, keeping all other entities and protocols internal as planned. This allowed unit tests to use the internal init method with dependency injection. More details on this approach can be found <a href="https://medium.com/better-programming/use-convenience-init-to-avoid-making-entities-public-in-your-package-in-swift-e51f6933e556">here</a>.</p><h4>Implementing the request method</h4><p>Networking implements the NetworkRequestProviding protocol and provides an async method to make network requests.</p><pre>public func executeRequest&lt;T: Decodable, V: NetworkRequestable&gt;(<br>  request: V,<br>  responseType: T.Type<br>) async -&gt; Result&lt;SuccessResponseWrapper&lt;T&gt;, NetworkingError&gt; {<br>  // some syntactic details are omitted for simplicity<br>  // make the URLRequest<br>  let urlRequest = requestFactory.makeURLRequest(with: request)<br>  // adapt request<br>  var adaptedRequest = urlRequest<br>  requestAdapters.forEach {<br>    adaptedRequest = $0.adapt(request: adaptedRequest)<br>  }<br>  // execute request<br>  let result = await requestExecutor.execute(adaptedRequest)<br>  // parse the result and return<br>  let parsedResult = responseParser.parseResult(result)<br>  return parsedResult<br>}</pre><p>We could keep the method and Networking lean by delegating all the work to sub-entities as shown above.</p><h4>Swift concurrency pitfalls</h4><p>We tried returning the result in the main actor to prevent consumers from having to switch to the main actor to update their UI, similar to using a completion handler on the main queue in GCD.</p><p>We were initially skeptical about adding the @MainActor annotation to the method since it only ensures that the method itself runs on the main actor, not the caller. However, we were surprised to find that the callers of the function continued to run on the main actor within their Task block, so we didn’t need to specify those tasks to run on the main actor, or so we thought.</p><p>It turns out, <a href="https://forums.swift.org/t/task-execution-behaviour-inconsistency-among-different-xcode-versions/60481">before Swift 5.7, the behavior was non-deterministic</a>, and after building our project with Xcode 14.0, we experienced local crashes due to UI updates on a background thread. We then removed the @MainActor annotation and updated networking interactions on the call site.</p><p>This change allowed consumers to switch to the main actor only when necessary, and to continue doing background work after retrieving the result from networking.</p><h4>URLSession Invalidation</h4><p>The Networking was functioning as intended at this stage, but we noticed the instance was still present in the memory after its intended scope. Something was wrong.</p><p>Although we initially found no apparent cause for a memory leak, we later realized that the system could cache the URLSession for future usage, retaining its delegate in the process. To prevent this behavior, we ensured proper deallocation by invalidating the URLSession in the deinit method.:</p><pre>deinit {<br>  session.finishTasksAndInvalidate()<br>}</pre><h4>Usage</h4><p>Now that we are done with the Networking implementation, let’s take a look at the complete usage, from request creation to result retrieval.</p><pre><br>let networking: NetworkRequestProviding<br><br>func makeRequest() {<br>  Task {<br>    let parameters = SampleRequestParameters(testProperty: &quot;test&quot;, secondTestProperty: &quot;test2&quot;)<br>    let request = SampleRequest(parameters: parameters)<br><br>    let result = await networking.makeRequest(<br>        request: request,<br>        responseType: ExampleType.self // A decodable<br>    )<br><br>    switch result {<br>      case .success(let successWrapper):<br>        await updateUI(successWrapper.expectedResponse)<br>      case .failure(let error):<br>        await showError(error)<br>    }<br>  }<br>}<br><br>@MainActor<br>func updateUI(with model: ExampleType.self) {<br>  // update UI safely on main actor<br>}<br><br>@MainActor<br>func showError(_ error: NetworkingError) {<br>}<br></pre><p>Swift concurrency makes requesting and processing results straightforward and readable, especially when compared to closure-based concurrency APIs.</p><h4>Mock networking for UI tests</h4><p>We also made a very cool Mock Networking feature with the ability to load mock JSON data, but that’s a story for another time — stay tuned for our next article! :)</p><h3>Final words</h3><p>We’re happy to have created a modern version of our Networking stack! We’ve built a highly scalable and easy-to-use library that’s been rigorously unit tested and leverages the new Swift concurrency. Along the way, we’ve learned a ton by overcoming various issues and challenges.</p><p>Thanks for reading this article — we hope you found it helpful! We’d love to hear your thoughts, so please join the discussion in the comments section. Until next time, take care! 👋</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bb1cdbf12725" width="1" height="1" alt=""><hr><p><a href="https://medium.com/getir/writing-a-modern-ios-networking-library-with-swift-concurrency-bb1cdbf12725">Writing a modern iOS Networking Library with Swift Concurrency</a> was originally published in <a href="https://medium.com/getir">Getir</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Use Convenience Init To Avoid Making Entities Public in Your Package in Swift]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/better-programming/use-convenience-init-to-avoid-making-entities-public-in-your-package-in-swift-e51f6933e556?source=rss-f1991f635894------2"><img src="https://cdn-images-1.medium.com/max/2600/0*JLBu_oHMpD6ytZ3T" width="3000"></a></p><p class="medium-feed-snippet">Create APIs that are more easily tested</p><p class="medium-feed-link"><a href="https://medium.com/better-programming/use-convenience-init-to-avoid-making-entities-public-in-your-package-in-swift-e51f6933e556?source=rss-f1991f635894------2">Continue reading on Better Programming »</a></p></div>]]></description>
            <link>https://medium.com/better-programming/use-convenience-init-to-avoid-making-entities-public-in-your-package-in-swift-e51f6933e556?source=rss-f1991f635894------2</link>
            <guid isPermaLink="false">https://medium.com/p/e51f6933e556</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[modularization]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[unit-testing]]></category>
            <dc:creator><![CDATA[Emre Havan]]></dc:creator>
            <pubDate>Tue, 10 Jan 2023 22:25:00 GMT</pubDate>
            <atom:updated>2023-01-10T22:25:00.134Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Implementing a Tracking System for iOS with CoreData]]></title>
            <link>https://emrehavan.medium.com/implementing-a-tracking-system-for-ios-with-coredata-1e3d24002e07?source=rss-f1991f635894------2</link>
            <guid isPermaLink="false">https://medium.com/p/1e3d24002e07</guid>
            <category><![CDATA[tracking-systems]]></category>
            <category><![CDATA[swift-programming]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[core-data]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <dc:creator><![CDATA[Emre Havan]]></dc:creator>
            <pubDate>Mon, 26 Oct 2020 14:31:02 GMT</pubDate>
            <atom:updated>2020-10-27T12:01:50.009Z</atom:updated>
            <content:encoded><![CDATA[<h4>An efficient implementation</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ann7KpyA0nGI6qk-odQsjg.png" /><figcaption>Photo by <a href="https://pixabay.com/users/mcmurryjulie-2375405/">mcmurryjulie</a> on <a href="https://pixabay.com/illustrations/database-search-database-search-icon-2797375/">Pixabay</a></figcaption></figure><p><em>Originally published at </em><a href="https://freeletics.engineering/2020/06/22/ios_tracking_coredata.html"><em>https://freeletics.engineering</em></a><em> on June 22, 2020.</em></p><p>As iOS developers, we often need to implement tracking in our applications. There are many third-party frameworks that would allow us to implement tracking systems in our projects. But in this article, we are going to talk about how we have implemented our custom tracking infrastructure at Freeletics with the help of CoreData, without using any third-party framework.</p><p>Our system will save each event generated by users, store them temporarily, and once the number of stored events reaches the defined limit, all the events are sent to the server. The client side tracking infrastructure is composed of three main entities: storage, batcher and sender.</p><ul><li>TrackingEventStorage: Responsible for storing, fetching and deleting events using CoreData.</li><li>TrackingEventsBatcher: Responsible for batching events and acting as a layer of communication between TrackingEventStorage and TrackingEventSender.</li><li>TrackingEventSender: Responsible for sending a list of events to the server.</li></ul><p>For the event itself, we have two different models. One is to store them in CoreData as NSManagedObject (ManagedInHouseTrackingEvent) and the other one is as a simple struct (InHouseTrackingEvent) to easily initialise from the consumer side and later to send to the backend. Our models look like the following:</p><h4>ManagedInHouseTrackingEvent:</h4><pre><strong>@objc</strong>(<strong>ManagedInHouseTrackingEvent</strong>)<br><strong>public</strong> <strong>final</strong> <strong>class</strong> <strong>ManagedInHouseTrackingEvent</strong>: <strong>NSManagedObject</strong> {<br><br>}<br><br><strong>extension</strong> <strong>ManagedInHouseTrackingEvent</strong> {<br><br>    <strong>@nonobjc</strong> <strong>public</strong> <strong>class</strong> <strong>func</strong> <strong>fetchRequest</strong>() <strong>-&gt;</strong> <strong>NSFetchRequest&lt;ManagedInHouseTrackingEvent&gt;</strong> {<br>        <strong>return</strong> <strong>NSFetchRequest&lt;ManagedInHouseTrackingEvent&gt;</strong>(entityName: <strong>String</strong>(describing: <strong>ManagedInHouseTrackingEvent.self</strong>))<br>    }<br><br>    <strong>@NSManaged</strong> <strong>public</strong> <strong>var</strong> name: <strong>String</strong>?<br>    <strong>@NSManaged</strong> <strong>public</strong> <strong>var</strong> properties: <strong>Data</strong>?<br>    <strong>@NSManaged</strong> <strong>public</strong> <strong>var</strong> id: <strong>String</strong>?<br>}<br><br><strong>extension</strong>  <strong>ManagedInHouseTrackingEvent</strong> {<br>    <strong>enum</strong> <strong>PropertyKey</strong>: <strong>String</strong> {<br>        <strong>case</strong> id<br>        <strong>case</strong> name<br>        <strong>case</strong> properties<br>    }<br>}</pre><h4>InHouseTrackingEvent:</h4><pre><strong>struct</strong> <strong>InHouseTrackingEvent</strong> {<br>    <strong>let</strong> id: <strong>String</strong><br>    <strong>let</strong> name: <strong>String</strong><br>    <strong>let</strong> properties: [<strong>String</strong>: <strong>Any</strong>]<br>}</pre><p>We normally do not need an id property for our events, but we will use it later while creating core data event models so that we can distinguish persisted events from each other later on.</p><p>As you can see, the properties field is of type Data in our managed model, whereas it is a [String: Any] dictionary in InHouseTrackingEvent. Since we are just going to use managed models to persist data rather than manipulating any existing ones, we are just going to convert properties to Data to easily persist them as Binary Data with CoreData.</p><h3>Event Storage Implementation</h3><p>After creating our models, and also xcdatamodel related to ManagedInHouseTrackingEvent, now we will continue with the core data stack.</p><p>We need to have a core data stack, where our storage class can initialise the managed context from its persistent container. Later we use this managed context to initialise NSEntityDescription that will describe our entity, and to interact with all CRUD operations for the database.</p><h4>InHouseTrackingCoreDataStack:</h4><pre><strong>final</strong> <strong>class</strong> <strong>InHouseTrackingCoreDataStack</strong> {<br><br>    <strong>static</strong> <strong>let</strong> shared <strong>=</strong> <strong>InHouseTrackingCoreDataStack</strong>()<br>    <strong>private</strong> <strong>let</strong> containerName <strong>=</strong> &quot;FreeleticsInHouseTracking&quot;<br><br>    <strong>private</strong> <strong>init</strong>() {}<br><br>    <strong>lazy</strong> <strong>var</strong> persistentContainer: <strong>NSPersistentContainer</strong> <strong>=</strong> {<br>        <strong>let</strong> container <strong>=</strong> <strong>NSPersistentContainer</strong>(name: containerName)<br>        container<strong>.loadPersistentStores</strong>(completionHandler: { [<strong>weak</strong> <strong>self</strong>] (_, error) <strong>in</strong><br>            <strong>if</strong> <strong>let</strong> self <strong>=</strong> <strong>self</strong>,<br>                <strong>let</strong> error <strong>=</strong> error <strong>as</strong> <strong>NSError</strong>? {<br>                <strong>print</strong>(&quot;Error!&quot;)<br>            }<br>        })<br>        <strong>return</strong> container<br>    }()<br>}</pre><h4>TrackingEventStorage:</h4><p>Now we can create TrackingEventStorage class. It will have four properties:</p><ul><li>entityName: Representing the class name for our core data model.</li><li>coreDataStack: A reference to our core data stack.</li><li>managedContext: An NSManagedObjectContext which will be used to wrap all CRUD operations for core data.</li><li>eventEntity: An NSEntityDescription representing our core data model.</li></ul><pre><strong>final</strong> <strong>class</strong> <strong>TrackingEventsStorage</strong> {<br><br>    <strong>let</strong> managedContext: <strong>NSManagedObjectContext</strong><br>    <strong>let</strong> eventEntity: <strong>NSEntityDescription</strong>?<br><br>    <strong>private</strong> <strong>let</strong> entityName <strong>=</strong> &quot;ManagedInHouseTrackingEvent&quot;<br>    <strong>private</strong> <strong>let</strong> coreDataStack <strong>=</strong> <strong>InHouseTrackingCoreDataStack.</strong>shared<br><br>    <strong>init</strong>() {<br>        managedContext <strong>=</strong> coreDataStack<strong>.</strong>persistentContainer<strong>.newBackgroundContext</strong>()<br>        eventEntity <strong>=</strong> <strong>NSEntityDescription.entity</strong>(forEntityName: entityName,<br>                                                 in: managedContext)<br>    }<br>}</pre><p>When we initialize the managedContext by using newBackgroundContext() from the persistent container, it will have the concurrencyType of privateQueueConcurrencyType. We want to have a dedicated managedContext so that whenever a database operation is done within, it will make sure every operation is executed on the same queue. We need this since CoreData is not thread-safe by default <a href="https://developer.apple.com/documentation/coredata/using_core_data_in_the_background#:~:text=Core%20Data%20is%20designed%20to,are%20associated%20with%20upon%20initialization.">[1]</a>. This will later allow us to safely interact with the tracking system regardless of what thread we are on. Moreover, we will be executing all core data related code inside a performAndWait <a href="https://www.kairadiagne.com/2019/01/06/understanding-the-core-data-perform-methods.html">[2]</a> closure of the managedContext. This will make sure all our operations will be executed synchronously. We need synchronicity since many of our actions will be depending on each other, such as making sure to check stored events after storing a new event.</p><p>We are going to implement three public methods for this class to interact with.</p><pre><strong>func</strong> <strong>storeEvent</strong>(_ event: <strong>InHouseTrackingEvent</strong>)<br><strong>func</strong> <strong>removeEvents</strong>(_ events: [<strong>InHouseTrackingEvent</strong>])<br><strong>func</strong> <strong>storedEvents</strong>(withMaximumAmountOf limit: <strong>Int</strong>?) <strong>-&gt;</strong> [<strong>InHouseTrackingEvent</strong>]?</pre><p>But before that we need to implement some private helper methods the public methods will benefit from.</p><p>First, we will need to implement a method to execute a given fetch request, which will perform the given request and return its results.</p><pre><strong>private</strong> <strong>func</strong> <strong>performFetchRequest</strong>(_ request: <strong>NSFetchRequest&lt;NSFetchRequestResult&gt;</strong>) <strong>-&gt;</strong> [<strong>NSManagedObject</strong>]? {<br>    <strong>var</strong> objects: [<strong>NSManagedObject</strong>]?<br><br>    managedContext<strong>.</strong>performAndWait {<br>        <strong>do</strong> {<br>            objects <strong>=</strong> <strong>try</strong> managedContext<strong>.fetch</strong>(request) <strong>as?</strong> [<strong>NSManagedObject</strong>]<br>        } <strong>catch</strong> {<br>            <strong>print</strong>(&quot;Error!&quot;)<br>        }<br>    }<br>    <strong>return</strong> objects<br>}</pre><p>We also need a method to create a fetch request to perform, which will have two parameters:</p><ul><li>identifiers: An optional array of identifiers to look for.</li><li>limit: An optional integer to set the limit of the fetch request.</li></ul><pre><strong>private</strong> <strong>func</strong> <strong>makeFetchRequest</strong>(withIDs identifiers: [<strong>String</strong>]? <strong>=</strong> <strong>nil</strong>,<br>                              withMaximumAmountOf limit: <strong>Int</strong>? <strong>=</strong> <strong>nil</strong>) <strong>-&gt;</strong> <strong>NSFetchRequest&lt;NSFetchRequestResult&gt;</strong> {<br>    <strong>let</strong> request <strong>=</strong> <strong>NSFetchRequest&lt;NSFetchRequestResult&gt;</strong>(entityName: entityName)<br>    <strong>if</strong> <strong>let</strong> identifiers <strong>=</strong> identifiers {<br>        request<strong>.</strong>predicate <strong>=</strong> <strong>NSPredicate</strong>(format: &quot;id IN %@&quot;, identifiers)<br>    }<br>    <strong>if</strong> <strong>let</strong> limit <strong>=</strong> limit {<br>        request<strong>.</strong>fetchLimit <strong>=</strong> limit<br>    }<br>    <strong>return</strong> request<br>}</pre><p>Next, we will implement the coreDataObjects method which will be retrieving stored NSManagedObjects with two parameters:</p><ul><li>identifiers: An optional array of identifiers to look for.</li><li>limit: An integer to set the limit of the fetch request. and by calling both the makeFetchRequest and performFetchRequest methods.</li></ul><pre><strong>private</strong> <strong>func</strong> <strong>coreDataObjects</strong>(withIDs identifiers: [<strong>String</strong>]? <strong>=</strong> <strong>nil</strong>,<br>                             withMaximumAmountOf limit: <strong>Int</strong>? <strong>=</strong> <strong>nil</strong>) <strong>-&gt;</strong> [<strong>NSManagedObject</strong>]? {<br>    <strong>let</strong> request <strong>=</strong> <strong>makeFetchRequest</strong>(withIDs: identifiers,<br>                                   withMaximumAmountOf: limit)<br><br>    <strong>return</strong> <strong>performFetchRequest</strong>(request)<br>}</pre><p>Another component we are going to need is a method to get InHouseTrackingEvent events from stored managed object events before providing those to upper-level APIs. We are going to create a factory class with makeEvent method for it as following:</p><pre><strong>final</strong> <strong>class</strong> <strong>InHouseTrackingEventFactory</strong> {<br><br>    <strong>typealias</strong> <strong>Keys</strong> <strong>=</strong> <strong>ManagedInHouseTrackingEvent.PropertyKey</strong><br><br>    <em>/// Initializes and returns an `InHouseTrackingEvent` from the given NSManagedObject</em><br>    <em>/// - Returns: Returns an InHouseTrackingEvent from NSManagedObject or nil if any error occurs</em><br>    <strong>static</strong> <strong>func</strong> <strong>makeEvent</strong>(from object: <strong>NSManagedObject</strong>) <strong>-&gt;</strong> <strong>InHouseTrackingEvent</strong>? {<br>        <strong>do</strong> {<br>            <strong>guard</strong> <strong>let</strong> propertiesData <strong>=</strong> object<strong>.value</strong>(forKey: <strong>Keys.</strong>properties<strong>.</strong>rawValue) <strong>as?</strong> <strong>Data</strong>,<br>                <strong>let</strong> properties <strong>=</strong> <strong>try</strong> <strong>JSONSerialization.jsonObject</strong>(with: propertiesData) <strong>as?</strong> [<strong>String</strong>: <strong>Any</strong>],<br>                <strong>let</strong> id <strong>=</strong> object<strong>.value</strong>(forKey: <strong>Keys.</strong>id<strong>.</strong>rawValue) <strong>as?</strong> <strong>String</strong>,<br>                <strong>let</strong> name <strong>=</strong> object<strong>.value</strong>(forKey: <strong>Keys.</strong>name<strong>.</strong>rawValue) <strong>as?</strong> <strong>String</strong> <strong>else</strong> {<br>                    <strong>return</strong> <strong>nil</strong><br>            }<br>            <strong>return</strong> <strong>InHouseTrackingEvent</strong>(id: id,<br>                                        name: name,<br>                                        properties: properties)<br>        } <strong>catch</strong> {<br>            <strong>print</strong>(&quot;Error!&quot;)<br>        }<br>    }<br>}</pre><p>Now we can add a method in TrackingEventStorage to convert all given managed object events into InHouseTrackingEvent:</p><pre><strong>private</strong> <strong>func</strong> <strong>events</strong>(from coreDataObjects: [<strong>NSManagedObject</strong>]) <strong>-&gt;</strong> [<strong>InHouseTrackingEvent</strong>]? {<br>    <strong>var</strong> events <strong>=</strong> [<strong>InHouseTrackingEvent</strong>]()<br>    managedContext<strong>.</strong>performAndWait {<br>        <strong>for</strong> coreDataObject <strong>in</strong> coreDataObjects {<br>            <strong>if</strong> <strong>let</strong> event <strong>=</strong> <strong>InHouseTrackingEventFactory.makeEvent</strong>(from: coreDataObject) {<br>                events<strong>.append</strong>(event)<br>            }<br>        }<br>    }<br>    <strong>return</strong> events<strong>.</strong>isEmpty ? nil : events<br>}</pre><p>Finally, we will implement a saveContext method to make sure any changes we made will be persisted in the database:</p><pre><strong>private</strong> <strong>func</strong> <strong>saveContext</strong>() {<br>    managedContext<strong>.</strong>performAndWait {<br>        <strong>do</strong> {<br>            <strong>guard</strong> managedContext<strong>.</strong>hasChanges <strong>else</strong> {<br>                <strong>return</strong><br>            }<br>            <strong>try</strong> managedContext<strong>.save</strong>()<br>        } <strong>catch</strong> {<br>            <strong>print</strong>(&quot;Error!&quot;)<br>        }<br>    }<br>}</pre><p>Now we are ready to implement our public methods mentioned before. These methods will allow other entities to interact with our core tracking mechanism.</p><p>Let&#39;s add a typealias to TrackingEventStorage class that we will use for our managed models property keys:</p><pre><strong>typealias</strong> <strong>Keys</strong> <strong>=</strong> <strong>ManagedInHouseTrackingEvent.PropertyKey</strong></pre><p>The first public method we are going to implement is storeEvent, which will persist given InHouseTrackingEvent as an NSManagedObject.</p><pre><strong>func</strong> <strong>storeEvent</strong>(_ event: <strong>InHouseTrackingEvent</strong>) {<br>    <strong>guard</strong> <strong>let</strong> eventEntity <strong>=</strong> eventEntity <strong>else</strong> {<br>        <strong>return</strong><br>    }<br><br>    managedContext<strong>.</strong>performAndWait {<br>        <strong>let</strong> managedEvent <strong>=</strong> <strong>NSManagedObject</strong>(entity: eventEntity, insertInto: managedContext)<br>        managedEvent<strong>.setValue</strong>(event<strong>.</strong>id, forKey: <strong>Keys.</strong>id<strong>.</strong>rawValue)<br>        managedEvent<strong>.setValue</strong>(event<strong>.</strong>name, forKey: <strong>Keys.</strong>name<strong>.</strong>rawValue)<br><br>        <strong>do</strong> {<br>            <strong>let</strong> propertyData <strong>=</strong> <strong>try</strong> <strong>JSONSerialization.data</strong>(withJSONObject: event<strong>.</strong>properties)<br>            managedEvent<strong>.setValue</strong>(propertyData, forKey: <strong>Keys.</strong>properties<strong>.</strong>rawValue)<br>        } <strong>catch</strong> {<br>            <strong>print</strong>(&quot;Error!&quot;)<br>            <strong>return</strong><br>        }<br>    }<br><br>    <strong>saveContext</strong>()<br><br>}</pre><p>Second one is removeEvents which accepts an array of InHouseTrackingEvent and removes corresponding managed model for each event in the array.</p><pre><strong>func</strong> <strong>removeEvents</strong>(_ events: [<strong>InHouseTrackingEvent</strong>]) {<br>    <strong>let</strong> eventIDs <strong>=</strong> events<strong>.</strong>map { $0<strong>.</strong>id }<br><br>    <strong>guard</strong> <strong>let</strong> coreDataObjects <strong>=</strong> <strong>coreDataObjects</strong>(withIDs: eventIDs) <strong>else</strong> {<br>        <strong>return</strong><br>    }<br><br>    managedContext<strong>.</strong>performAndWait {<br>        coreDataObjects<strong>.</strong>forEach { <strong>self.</strong>managedContext<strong>.delete</strong>($0) }<br>    }<br><br>    <strong>saveContext</strong>()<br>}</pre><p>Last public method is storedEvents which accepts limit parameter to return stored managed models with the maximum amount of limit.</p><pre><strong>func</strong> <strong>storedEvents</strong>(withMaximumAmountOf limit: <strong>Int</strong>?) <strong>-&gt;</strong> [<strong>InHouseTrackingEvent</strong>]? {<br>    <strong>guard</strong> <strong>let</strong> objects <strong>=</strong> <strong>coreDataObjects</strong>(withMaximumAmountOf: limit) <strong>else</strong> {<br>        <strong>return</strong> <strong>nil</strong><br>    }<br><br>    <strong>return</strong> <strong>events</strong>(from: objects)<br>}</pre><h3>Event Sender Implementation</h3><p>We are going to omit implementation details for the event-sending class for simplicity. InHouseTrackingEventSender is going to have a method to send events that will accept an array of InHouseTrackingEvent and make an URL request to send them to the backend. Moreover, it is going to have a weak delegate property of type TrackingEventSenderDelegate which will be needed to notify once events have successfully submitted to the backend. As you probably noticed, errors are not handled explicitly. If something goes wrong, we simply do nothing and send the same events later on.</p><h4>InHouseTrackingEventSender:</h4><pre><strong>final</strong> <strong>class</strong> <strong>InHouseTrackingEventSender</strong> {</pre><pre>    <strong>weak</strong> <strong>var</strong> delegate: <strong>TrackingEventSenderDelegate</strong>?</pre><pre>    <strong>func</strong> <strong>sendEvents</strong>(_ events: [<strong>InHouseTrackingEvent</strong>]) {<br>        <em>// Make sure there are no ongoing requests and make a</em><br>        <em>// post request to the backend by including each event in</em><br>        <em>// the body of the request.</em><br></pre><pre>        <em>// success:</em><br>        delegate?<strong>.didSendEvents</strong>(events)</pre><pre>        <em>// error:</em><br>        <em>// Handle error</em><br>    }<br>}</pre><h4>TrackingEventSenderDelegate:</h4><pre><strong>protocol</strong> <strong>TrackingEventSenderDelegate</strong>: <strong>class</strong> {<br>  <strong>func</strong> <strong>didSendEvents</strong>(_ events: [<strong>InHouseTrackingEvent</strong>])<br>}</pre><h3>Event Batcher Implementation</h3><p>It is time for us to implement the last part of our tracking service. We need a batching mechanism to make sure our tracking system will work by taking performance, battery, and real-time tracking into account. By providing a batch size, we will try to have an ideal balance between performance and real-time tracking by not triggering a URL request for each event stored, but only triggering once the stored event number meets the batch size. It is going to be a singleton and going to be used to directly track an event. Before implementing the batcher singleton, let&#39;s write a simple struct which will be responsible for providing the batch size. We could hardcode this value but providing it via another entity can make it easier and clearer to maintain this information, especially if it can be updated via remote configurations.</p><pre><strong>struct</strong> <strong>TrackingEventsBatchSizeProvider</strong> {<br>    <strong>let</strong> defaultBatchSize <strong>=</strong> 20<br>}</pre><pre><strong>extension</strong> <strong>TrackingEventsBatchSizeProvider</strong>: <strong>TrackingEventsBatchSizeProviding</strong> {<br>    <strong>var</strong> batchSize: <strong>Int</strong> {<br>        <em>// We just return default size for simplicity but we could get some remote config value</em><br>        <em>// at this point and provide it as well.</em><br>        <strong>return</strong> defaultBatchSize<br>    }<br>}</pre><h4>TrackingEventsBatcher:</h4><p>Now we can create the batcher singleton, TrackingEventsBatcher.</p><p>It will be initialised with four properties:</p><ul><li>shouldBatchEvents: A boolean to indicate if events should be batched or sent immediately.</li><li>eventStorage: An instance of TrackingEventStorage.</li><li>eventSender: An instance of InHouseTrackingEventSender.</li><li>batchSizeProvider: A struct to provide how big the batch size should be.</li></ul><p>It will also conform to TrackingEventSenderDelegate to set itself as the delegate of the initialised event sender class.</p><pre><strong>final</strong> <strong>class</strong> <strong>TrackingEventsBatcher</strong>: <strong>TrackingEventSenderDelegate</strong> {</pre><pre>    <strong>static</strong> <strong>let</strong> shared <strong>=</strong> <strong>TrackingEventsBatcher</strong>()</pre><pre>    <strong>var</strong> shouldBatchEvents <strong>=</strong> <strong>true</strong></pre><pre>    <strong>private</strong> <strong>var</strong> eventStorage: <strong>TrackingEventStoring</strong><br>    <strong>private</strong> <strong>var</strong> eventSender: <strong>TrackingEventSending</strong><br>    <strong>private</strong> <strong>var</strong> batchSizeProvider: <strong>TrackingEventsBatchSizeProviding</strong></pre><pre>    <strong>init</strong>(eventStorage: <strong>TrackingEventStoring</strong> <strong>=</strong> <strong>TrackingEventsStorage</strong>(),<br>         eventSender: <strong>TrackingEventSending</strong> <strong>=</strong> <strong>InHouseTrackingEventSender</strong>(),<br>         batchSizeProvider: <strong>TrackingEventsBatchSizeProviding</strong> <strong>=</strong> <strong>TrackingEventsBatchSizeProvider</strong>()) {<br>        <strong>self.</strong>eventStorage <strong>=</strong> eventStorage<br>        <strong>self.</strong>eventSender <strong>=</strong> eventSender<br>        <strong>self.</strong>batchSizeProvider <strong>=</strong> batchSizeProvider<br>        <strong>self.</strong>eventSender<strong>.</strong>delegate <strong>=</strong> <strong>self</strong><br>    }</pre><pre>    <strong>func</strong> <strong>didSendEvents</strong>(_ events: [<strong>InHouseTrackingEvent</strong>]) {<br>        <em>// empty for now</em><br>    }<br>}</pre><p>As you can see, shouldBatchEvents is a public property so that it can later be modified. Control with this flag will allow us to either submit tracked events immediately or batch them until we hit the batch size. For the simplicity of this article, it will always be true.</p><p>Now we will add 2 helper private methods, the first one is to determine if the events should be sent, and another one to send events if needed:</p><pre><strong>private</strong> <strong>func</strong> <strong>shouldSendEvents</strong>(_ events: [<strong>InHouseTrackingEvent</strong>]) <strong>-&gt;</strong> <strong>Bool</strong> {<br>    <em>// Send events if they shouldn&#39;t be batched, regardless of their number</em><br>    <em>// or only if their number is greater than the batch size, if they should be batched.</em><br>    <strong>return</strong> <strong>!</strong>shouldBatchEvents <strong>||</strong> events<strong>.</strong>count <strong>&gt;=</strong> batchSizeProvider<strong>.</strong>batchSize<br>}</pre><pre><strong>private</strong> <strong>func</strong> <strong>sendEventsIfNeeded</strong>() {<br>    <strong>guard</strong> <strong>let</strong> storedEvents <strong>=</strong> eventStorage<strong>.storedEvents</strong>(withMaximumAmountOf: batchSizeProvider<strong>.</strong>batchSize),<br>        <strong>shouldSendEvents</strong>(storedEvents) <strong>else</strong> {<br>            <strong>return</strong><br>    }<br>    eventSender<strong>.sendEvents</strong>(storedEvents)<br>}</pre><p>We first fetch stored events with batchSize limit and then see if we should be sending events already.</p><p>Now we will implement the method which will be the entry point of our whole tracking infrastructure, the following method will be called throughout the application where an entity needs to track an event.</p><pre><strong>func</strong> <strong>batchEvent</strong>(_ event: <strong>InHouseTrackingEvent</strong>) {<br>    eventStorage<strong>.storeEvent</strong>(event)<br>    <strong>sendEventsIfNeeded</strong>()<br>}</pre><p>Whenever an event is tracked through batchEvent, we will store it and check if events should be sent.</p><p>Finally, we will update didSendEvents method as following:</p><pre><strong>func</strong> <strong>didSendEvents</strong>(_ events: [<strong>InHouseTrackingEvent</strong>]) {<br>    eventStorage<strong>.removeEvents</strong>(events)</pre><pre>    <strong>sendEventsIfNeeded</strong>()<br>}</pre><p>We make sure all submitted events are removed from storage and check if more events should be sent. This logic is needed because the number of stored events might have been more than twice the batch size. This can occur when the app is used offline and no events have been sent for a while.</p><h3>Usage</h3><p>Let&#39;s see how we can interact with the system with a sample class:</p><pre><strong>class</strong> <strong>SampleEntity</strong> {<br>    <strong>func</strong> <strong>trackSomething</strong>() {<br>        <strong>let</strong> eventName <strong>=</strong> &quot;example_event&quot;<br>        <strong>let</strong> id <strong>=</strong> &quot;\(eventName)_\(<strong>Date</strong>()<strong>.</strong>timeIntervalSince1970)&quot;<br>        <strong>let</strong> properties: [<strong>String</strong>: <strong>Any</strong>] <strong>=</strong> [<br>            &quot;propertyOne&quot;: &quot;1&quot;,<br>            &quot;propertyTwo&quot;: <strong>true</strong><br>        ]<br>        <strong>let</strong> event <strong>=</strong> <strong>InHouseTrackingEvent</strong>(id: id,<br>                                         name: eventName,<br>                                         properties: properties)<br>        <strong>TrackingEventsBatcher.</strong>shared<strong>.batchEvent</strong>(event)<br>    }<br>}</pre><p>Usually, we have different entities for different events in our applications and this manual conversion of properties can be prevented by providing a mechanism to convert properties into required dictionary format through event entities. But for simplicity, we just add two random properties and show how it can be batched here. We could also implement a wrapper function called track, which could internally handle batching as well.</p><h3>Further improvements for TrackingEventStorage</h3><p>There are a few more things we need to consider for the TrackingEventStorage. Especially for the saveContext() method. There is a property named isProtectedDataAvailable which lives inside UIApplication. This property will help us to determine if there is data protection active or the device is locked. For such cases we should not attempt to do database operations, otherwise, we might experience some crashes <a href="https://developer.apple.com/documentation/uikit/uiapplication/1622925-isprotecteddataavailable">[3]</a>.</p><p>Let’s add the check for this property as we check if there are any changes as well (in saveContext):</p><pre><strong>guard</strong> <strong>UIApplication.</strong>shared<strong>.</strong>isProtectedDataAvailable,<br>    managedContext<strong>.</strong>hasChanges <strong>else</strong> {<br>    <strong>return</strong><br>}</pre><p>One could expect this to work right away but now we have another problem. We have implemented our tracking mechanism as thread-safe but we should only be checking UIApplication.shared.isProtectedDataAvailable from the main queue. Thus, we need to check on which queue we are in before attempting to read this value and synchronise with the main if necessary. We could just do if Thread.isMainThread check, but we are going to go with a different solution instead since this check might not just be enough and safe to make sure we can synchronise with the main queue <a href="http://blog.benjamin-encz.de/post/main-queue-vs-main-thread/">[4]</a>.</p><p>We are going to use a refactored version of this <a href="https://stackoverflow.com/questions/17475002/get-current-dispatch-queue/60314121#60314121">post</a> to determine which dispatch queue we are running on properly.</p><h4>DispatchQueue extension:</h4><pre><strong>import</strong> <strong>Foundation</strong></pre><pre><em>// Reference https://stackoverflow.com/a/60314121/8447312</em><br><strong>public</strong> <strong>extension</strong> <strong>DispatchQueue</strong> {</pre><pre>    <strong>static</strong> <strong>var</strong> current: <strong>DispatchQueue</strong>? { <strong>getSpecific</strong>(key: key)?<strong>.</strong>queue }</pre><pre>    <strong>private</strong> <strong>struct</strong> <strong>QueueReference</strong> {<br>        <strong>weak</strong> <strong>var</strong> queue: <strong>DispatchQueue</strong>?<br>    }</pre><pre>    <strong>private</strong> <strong>static</strong> <strong>let</strong> key: <strong>DispatchSpecificKey&lt;QueueReference&gt;</strong> <strong>=</strong> {<br>        <strong>let</strong> key <strong>=</strong> <strong>DispatchSpecificKey&lt;QueueReference&gt;</strong>()<br>        <strong>setUpSystemQueuesDetection</strong>(key: key)<br>        <strong>return</strong> key<br>    }()</pre><pre>    <strong>private</strong> <strong>static</strong> <strong>func</strong> <strong>setUpSystemQueuesDetection</strong>(key: <strong>DispatchSpecificKey&lt;QueueReference&gt;</strong>) {<br>        <strong>let</strong> queues: [<strong>DispatchQueue</strong>] <strong>=</strong> [<br>            <strong>.</strong>main,<br>            <strong>.global</strong>(qos: <strong>.</strong>background),<br>            <strong>.global</strong>(qos: <strong>.default</strong>),<br>            <strong>.global</strong>(qos: <strong>.</strong>unspecified),<br>            <strong>.global</strong>(qos: <strong>.</strong>userInitiated),<br>            <strong>.global</strong>(qos: <strong>.</strong>userInteractive),<br>            <strong>.global</strong>(qos: <strong>.</strong>utility)<br>        ]<br>        <strong>registerDetection</strong>(of: queues, key: key)<br>    }</pre><pre>    <strong>private</strong> <strong>static</strong> <strong>func</strong> <strong>registerDetection</strong>(of queues: [<strong>DispatchQueue</strong>], key: <strong>DispatchSpecificKey&lt;QueueReference&gt;</strong>) {<br>        queues<strong>.</strong>forEach {<br>            $0<strong>.setSpecific</strong>(key: key,<br>                           value: <strong>QueueReference</strong>(queue: $0))<br>        }<br>    }<br>}</pre><p>Now we will add a new method to TrackingEventsStorage to check if isProtectedDataAvailable properly:</p><pre><strong>private</strong> <strong>func</strong> <strong>isProtectedDataAvailable</strong>() <strong>-&gt;</strong> <strong>Bool</strong> {<br>    <strong>var</strong> isProtectedDataAvailable <strong>=</strong> <strong>false</strong></pre><pre>    <strong>if</strong> <strong>DispatchQueue.</strong>current <strong>==</strong> <strong>DispatchQueue.</strong>main {<br>        isProtectedDataAvailable <strong>=</strong> <strong>UIApplication.</strong>shared<strong>.</strong>isProtectedDataAvailable<br>    } <strong>else</strong> {<br>        <strong>DispatchQueue.</strong>main<strong>.</strong>sync {<br>            isProtectedDataAvailable <strong>=</strong> <strong>UIApplication.</strong>shared<strong>.</strong>isProtectedDataAvailable<br>        }<br>    }<br>    <strong>return</strong> isProtectedDataAvailable<br>}</pre><p>Let’s change the saveContext method to the following, in order to make use UIApplication.shared.isProtectedDataAvailable is true before saving the context.</p><pre><strong>private</strong> <strong>func</strong> <strong>saveContext</strong>() {<br>    <strong>let</strong> protectedDataAvailable <strong>=</strong> <strong>isProtectedDataAvailable</strong>()<br>    managedContext<strong>.</strong>performAndWait {<br>        <strong>do</strong> {<br>            <strong>guard</strong> protectedDataAvailable,<br>                managedContext<strong>.</strong>hasChanges <strong>else</strong> {<br>                    <strong>return</strong><br>            }<br>            <strong>try</strong> managedContext<strong>.save</strong>()<br>        } <strong>catch</strong> {<br>            <strong>print</strong>(&quot;Error!&quot;)<br>        }<br>    }<br>}</pre><p>One thing to note is that we are doing queue changing, if necessary, outside of the performAndWait closure. It is needed since perform and performAndWait closures should only be used for changes related to NSManagedObjects.</p><h3>Conclusion</h3><p>In this article, we have seen how CoreData can be used for a custom event tracking system implementation. We have built a system to persist events temporarily on the device and submit them to the backend in batches. We have also made sure that such a system can be accessed from different threads/queues and explored ways of properly determining the current queue of the execution.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1e3d24002e07" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Use Enums and Associated Values to Parse JSON in Swift]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/better-programming/parse-items-with-different-key-value-pairs-in-a-json-array-with-the-help-of-enums-and-associated-301ffa81179e?source=rss-f1991f635894------2"><img src="https://cdn-images-1.medium.com/max/2600/1*UFPiPGQ8lf5scIVjpQAmkg.jpeg" width="5760"></a></p><p class="medium-feed-snippet">Parse items with different key-value pairs</p><p class="medium-feed-link"><a href="https://medium.com/better-programming/parse-items-with-different-key-value-pairs-in-a-json-array-with-the-help-of-enums-and-associated-301ffa81179e?source=rss-f1991f635894------2">Continue reading on Better Programming »</a></p></div>]]></description>
            <link>https://medium.com/better-programming/parse-items-with-different-key-value-pairs-in-a-json-array-with-the-help-of-enums-and-associated-301ffa81179e?source=rss-f1991f635894------2</link>
            <guid isPermaLink="false">https://medium.com/p/301ffa81179e</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[mobile]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[ios]]></category>
            <dc:creator><![CDATA[Emre Havan]]></dc:creator>
            <pubDate>Mon, 03 Feb 2020 20:37:55 GMT</pubDate>
            <atom:updated>2021-04-19T14:52:09.670Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[How to deploy a Review Classifier in ANY application]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-snippet">You can classify your dataset but how to make it useful for future applications?</p><p class="medium-feed-link"><a href="https://medium.com/data-science/how-to-deploy-a-review-classifier-in-any-application-c1c0e5a0e8ff?source=rss-f1991f635894------2">Continue reading on TDS Archive »</a></p></div>]]></description>
            <link>https://medium.com/data-science/how-to-deploy-a-review-classifier-in-any-application-c1c0e5a0e8ff?source=rss-f1991f635894------2</link>
            <guid isPermaLink="false">https://medium.com/p/c1c0e5a0e8ff</guid>
            <category><![CDATA[application-development]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[classification]]></category>
            <dc:creator><![CDATA[Emre Havan]]></dc:creator>
            <pubDate>Mon, 20 Jan 2020 07:12:01 GMT</pubDate>
            <atom:updated>2021-08-23T14:02:49.050Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Recommender System Application Development]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/data-science/recommender-system-application-development-part-1-of-4-cosine-similarity-f6dbcd768e83?source=rss-f1991f635894------2"><img src="https://cdn-images-1.medium.com/max/1132/1*WQkVJMIfz8Iqkg3-hHnabg.png" width="1132"></a></p><p class="medium-feed-snippet">Cosine Similarity, Rating thresholding and other custom techniques</p><p class="medium-feed-link"><a href="https://medium.com/data-science/recommender-system-application-development-part-1-of-4-cosine-similarity-f6dbcd768e83?source=rss-f1991f635894------2">Continue reading on TDS Archive »</a></p></div>]]></description>
            <link>https://medium.com/data-science/recommender-system-application-development-part-1-of-4-cosine-similarity-f6dbcd768e83?source=rss-f1991f635894------2</link>
            <guid isPermaLink="false">https://medium.com/p/f6dbcd768e83</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[cosine-similarity]]></category>
            <category><![CDATA[recommendation-system]]></category>
            <category><![CDATA[recommender-systems]]></category>
            <dc:creator><![CDATA[Emre Havan]]></dc:creator>
            <pubDate>Sat, 07 Dec 2019 00:01:31 GMT</pubDate>
            <atom:updated>2021-08-23T14:04:20.798Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Recommender System (Öneri Sistemi) Uygulaması Geliştirme Bölüm 1/4: Cosine Similarity]]></title>
            <link>https://emrehavan.medium.com/recommender-system-%C3%B6neri-sistemi-uygulamas%C4%B1-geli%C5%9Ftirme-b%C3%B6l%C3%BCm-1-4-cosine-similarity-1323e3b5b6ae?source=rss-f1991f635894------2</link>
            <guid isPermaLink="false">https://medium.com/p/1323e3b5b6ae</guid>
            <category><![CDATA[cosine-similarity]]></category>
            <category><![CDATA[recommendation-system]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Emre Havan]]></dc:creator>
            <pubDate>Thu, 05 Dec 2019 23:18:38 GMT</pubDate>
            <atom:updated>2019-12-09T22:50:18.335Z</atom:updated>
            <content:encoded><![CDATA[<h3>Cosine Similarity, Rating eşiği ve matematiksel formüllerle Recommender System (Öneri Sistemi) Uygulaması Geliştirme</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/709/1*YSDKzjONGi1xB6ub2gidXw.jpeg" /><figcaption><a href="https://labs.criteo.com/2017/08/recruiter-love-recommender-systems/">Kaynak</a></figcaption></figure><p>Merhabalar,</p><p>Bu yazıda, Python, Cosine Similarity ve diğer matematiksel fonksiyonlar kullanarak öneri uygulaması (Recommender System, Recommender Engine) geliştireceğiz. Bu uygulama yüksek lisans bitirme projemde geliştirdiğim uygulamanın bir kısmını içeriyor olacak. Eğer dilerseniz bitirme projemin kaynak koduna <a href="https://github.com/emrepun/MSCProject">buradan</a> erişebilirsiniz.</p><p>Birçok farklı yol izleyerek öneri uygulaması geliştirmek mümkün ama bizim burada inceleyeceğimiz yöntemler Cold Start problemine çözüm öneren bir yaklaşım olacak. Cold Start problemi, kullanıcı hakkında hiçbir bilgiye sahip değilken (yeni kaydolmuş bir kullanıcı) öneri yapma sorunudur. Bu projemizde kullanıcıdan çok az bilgi alarak (örneğin kullanıcının yalnızca bir kategoriyi seçmesi) elde var olan datadan mantıklı öneriler yapmaya çalışacağız.</p><p>Gerekenler:</p><ul><li>Python 3</li><li>numpy</li><li>pandas</li><li>nltk</li></ul><blockquote>Bu yazımızda Python programlama dilini bildiğinizi varsayarak ilerleyeceğim, ayrıca bu serideki asıl amaç öneri uygulaması ve farklı formüller ve tekniklerle öneri yapan bir sistem oluşturmak olduğu için, yazılan Python kodunun detaylarına fazla girmeyeceğim. Ayrıca Python dilinde çok uzman olmadığım için yazdığım bazı kısımlar muhtemelen daha iyi yazılabilirdi. Eğer değiştirmemi istediğiniz bir kısım olursa yorum yazarak beni haberdar ederseniz memnun olurum :)</blockquote><p>4 farklı versiyon geliştirerek her birinde öneri sistemimizi farklı açıdan geliştirip daha kapmsalı bir hale getireceğiz. İlk versiyonumuzda Cosine Similarity kullanarak öneri yapacağız.</p><p>Yazacağımız öneri uygulaması, verilen gezi türüne göre 5 farklı şehir öneren bir sistem olacak. Tabi bu uygulama için izleyeceğimiz yöntemleri uygulayarak başka türde öneriler yapan sistemler geliştirmekte mümkün.</p><p>Sistemin baz alacağı veri setini <a href="https://github.com/emrepun/RecommenderEngine/blob/master/data_sets/city_data.csv">buradan</a> indirebilirsiniz.</p><p>Elimizdeki veri setindeki bazı özellikler (feature) gerçek verilerken bazıları ise deneme amaçlı benim rastgele eklediğim verilerden oluşuyor. Genel olarak veri setimiz 25 sehirden olusan bir veri seti. Setimizdeki her veri şu featurlara sahip: <strong>city, popularity, description, image, rating, rating_count, positive_review, negative_review.</strong> Veri setimizdeki özellikler ve değerleri ilk 5 şehir için aşağıda ki resimde gösterilmektedir.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3nPAeZ_SSlO30FuhtWtdbQ.png" /><figcaption>Şehir veri seti özeti</figcaption></figure><p>Yukarda belirttiğim featurelardan city, popularity, description ve image bilgilerini Tripadvisor sitesindeki şehir bilgilerini yansıtıyor (tabi zamanla bilgiler siteden farklılık gösterecektir). Bunlar haricinde olan rating, rating_count, positive_review ve negative_review featureları ise rastgele değerler içeriyor. Dilerseniz her bir feature’ın ne işe yaradığını tek tek inceleyelim.</p><ul><li>city: Şehir ismi</li><li>popularity: Şehir için kaydedilmiş review sayısı</li><li>description: Şehir hakkında bilgi veren küçük blog yazısı</li><li>image: Şehir background image url</li><li>rating: Şehrin sahip olduğu ortalama rating değeri 0–10 skalasında</li><li>rating_count: Kaç kullanıcıdan rating alındığı</li><li>postive_review: Girilen pozitif inceleme sayısı</li><li>negative_review: Girilen negatif inceleme sayısı</li></ul><p>Veri setimizi ve sahip olduğumuz featureları öğrendiğimize göre artık başlayabiliriz. İlk versiyonumuzda yalnızca, city ve description featurelarini kullanarak ilerleyeceğiz.</p><h3>Versiyon-1</h3><p>İlk geliştireceğimiz versiyon yalnızca veri setindeki girdilerin description (açıklama) feature’ını baz alarak öneriler verecek bir sistem olacak. Bu sistem kullanıcının seçtiği gezi türüne bağlı olan keywordler (anahtar kelime) ile şehir açıklamaları arasındaki cosine similarity (cosinus benzerliği) değerini hesaplayarak en yüksek değere sahip olan 5 şehri kullanıcılara önerecek.</p><h4>Cosine Similarity</h4><p>Yukarda belirtilen benzerliği Cosine Similarity kullanarak hesaplayacağız. Cosine Similarity iki vektör arasındaki cosinus açısını çok boyutlu bir uzayda hesaplayarak vektörler arasındaki benzerliği ölçme metodudur. Cosine Similarity iki vektörün dot productunun, vektörlerin büyüklüklerinin çarpımına bölünmesiyle elde edilir. Aşağıda Cosine Similarity formülünü ve iki vektör arasındaki açıyı görsel olarak ifade eden figür’ü görebilirsiniz. 2 vektör arasındaki açı ne kadar küçükse, bu iki vektör birbirine o derecede benzerdir diyebiliriz. Bu method’a projemizin ilerleyen kısımlarında tekrar değineceğiz ama daha detaylı bilgi almak için <a href="http://bilgisayarkavramlari.sadievrenseker.com/2010/04/02/matris-carpimi-matrix-multiplication/">[1]</a>, <a href="https://youtu.be/oDR8cAV-2J4?t=211">[2]</a> referanslarına bakabilirsiniz.</p><figure><img alt="Cosine Similarity Formülü" src="https://cdn-images-1.medium.com/max/724/1*hj5lwGB3SA2CVrkSHMgykA.png" /><figcaption>A ve B vektörleri için Cosine Similarity Formülü</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/502/1*cv4PpPQLGgrcf1Nu7tvY3Q.png" /><figcaption>3 boyutlu uzayda A ve B vektörleri arasındaki benzerlik</figcaption></figure><h4>Preprocessing (Ön işlem)</h4><p>Öncelikle elimizde olan veri setini kullanıma hazır bir hale getirebilmek için bazı işlemler gerçekleştirmemiz gerekiyor. Veri setimizinde içinde bulunduğu bir klasörde pre_processing.py adında bir Python dosyası oluşturalım.</p><p>Öncelikli olarak geliştireceğimiz recommender system sadece description feature’ını baz alarak önerilerde bulunacağı için veri setimizdeki girdilerin description featurelar’ını biraz temizlememiz gerekiyor.</p><p>İlk olarak ingilizcede stop words olarak adlandırılan kelimeleri şehir betimlemelerinden sileceğiz. Stop words, her yazıda sıkça rastlanan ama kendi başına pek bir anlam ifade etmeyen kelimelere denmektedir. (the, for, an, a, or, what vb). Bunu yapmamızdaki amaç, gereksiz olan kelimelerin çok fazla geçtiği şehir betimlemelerindeki benzerlik skorunun düşmesini engellemek. Çünkü cosine similarity hesaplamasında betimlemelerdeki her bir kelime uzayda ayrı bir boyut oluşturacağı için, bu tarz gereksiz kelimeler benzerlik skorunu negatif yönde etkileyebilir.</p><pre>import numpy as np<br>import pandas as pd<br>from nltk.corpus import stopwords</pre><pre>def clear(city):<br>    city = city.lower()<br>    city = city.split()<br>    city_keywords = [word for word in city if word not in stopwords.words(&#39;english&#39;)]</pre><pre>    merged_city = &quot; &quot;.join(city_keywords)<br>    return merged_city</pre><p>Yukardaki clear methodu sayesinde veri setimizdeki şehir betimlemelerini temizleyebiliriz. Method sırasıyla şu şekilde çalışıyor:</p><ul><li>city adında bir String parametresi kabul ediyor</li><li>Alınan Stringde ki tüm harfleri öncelikle .lower() methodu yardımıyla küçültüyor</li><li>Sonrasında .split() ile tüm kelimeleri ayırarak bir String listesi oluşturuyor</li><li>Sonrasında ise stopwords içersinde olan tüm kelimeleri bu listeden çıkararak city_keywords değişkenini elde ediyoruz.</li><li>Daha sonra tüm bu kelimeleri aralarında boşluk bırakarak bir String haline getirip (merged_city) methodun çağrıldığı yere dönüyor.</li></ul><p>Şimdi bu metodu veri setimizdeki her bir şehre uygulayarak betimlemeleri temizleyelim. Aşağıdaki kod bloğunu clear metodunun altına ekleyelim:</p><pre>for index, row in df.iterrows():<br>    clear_desc = clear(row[&#39;description&#39;])<br>    df.at[index, &#39;description&#39;] = clear_desc</pre><pre>updated_dataset = df.to_csv(&#39;city_data_cleared.csv&#39;)</pre><p>Bu kod bloğu her bir şehir betimlemesini temizleyerek, temizlenmiş verileri city_data_cleared.csv adında bir dosyaya kaydedecek. Bundan sonraki işlemlerimizde o veri setini kullanacağız.</p><p>pre_processing.py gist:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a0503ffe039790bbd29611fd04587551/href">https://medium.com/media/a0503ffe039790bbd29611fd04587551/href</a></iframe><h4>Cosine Similarity ile benzerlik hesaplama</h4><p>Artık şehir betimlemelerini temizlediğimize gore, benzerlik skorunu hesaplayacak olan metodu yazmaya baslayabiliriz. <strong>cosine_similarity.py </strong>adında bir python dosyasi olusturalim.</p><p>Daha onceden belirttiğim gibi, yazacağımız metod iki ayrı string deki kelimelerin benzerliğine gore skor veren bir metod olacak. İlk olarak verilen bu iki ayrı String kelimelerden olusan vektörlere cevireceğiz. Sonrasında herhangi bir vektörde var olan herhangi bir kelime uzayda ayrı bir boyut oluşturacak, ve eğer bir vektörde var olan bir kelime diğer vektörde yoksa, o kelime icin öteki vektörün uzayındaki kelimenin boyutuna karşılık gelen deger 0 olacak.</p><blockquote><strong>Not:</strong> Cosine similarity, kelimelerin bir metinde birden fazla gecmesinden çok etkilenmeyen bir metod, bizim açımızdan kelimelerin bir kere geciyor olması yeterli olacağı icin bunun pek bir onemi yok, ama eğer ilerde siz başka bir uygulamada buna önem vermek isterseniz, <a href="https://en.wikipedia.org/wiki/Pearson_correlation_coefficient">Pearson correleation</a> metoduna bakmanızı öneririm.</blockquote><p>Daha generic ve tekrar kullanilabilir olması icin cosine_similarity.py dosyamıza yazacağımız kodlari CosineSimilarity adında bir sınıf altında yazalım:</p><pre>import re, math<br>from collections import Counter</pre><pre>class CosineSimilarity:<br>    def __init__(self):<br>        print(&quot;Cosine Similarity initialized&quot;)<br>    <br>    <a href="http://twitter.com/staticmethod">@staticmethod</a><br>    def cosine_similarity_of(text1, text2):<br>        first = re.compile(r&quot;[\w&#39;]+&quot;).findall(text1)<br>        second = re.compile(r&quot;[\w&#39;]+&quot;).findall(text2)<br>        vector1 = Counter(first)<br>        vector2 = Counter(second)</pre><pre>        common = set(vector1.keys()).intersection(set(vector2.keys()))</pre><pre>        dot_product = 0.0</pre><pre>        for i in common:<br>          <br>            dot_product += vector1[i] * vector2[i]</pre><pre>        squared_sum_vector1 = 0.0<br>        squared_sum_vector2 = 0.0</pre><pre>        for i in vector1.keys():<br>            squared_sum_vector1 += vector1[i]**2</pre><pre>        for i in vector2.keys():<br>            squared_sum_vector2 += vector2[i]**2</pre><pre>        magnitude = math.sqrt(squared_sum_vector1) * math.sqrt(squared_sum_vector2)</pre><pre>        if not magnitude:<br>           return 0.0<br>        else:<br>           return float(dot_product) / magnitude</pre><p>Yazdığımız cosine_similarity_of metodu aşağıdaki sekilde çalışıyor:</p><ul><li>Öncelikle verilen iki Stringi Regex yardımıyla kelimelerine ayırıyor</li><li>Daha sonra iki string icinde iki ayrı, kelimeleri ve kaç kere geçtiklerini içeren bir dictionary oluşturuyor (örn: nice: 4)</li><li>Daha sonra her iki vektörde de ortak bulunan kelimeleri elde ediyor</li><li>Yukarda Cosine Similarity başlığı altında verilen formulu izleyerek benzerliği hesaplayıp bunu metodun sonunda donuyor</li></ul><p>cosine_similarity.py gist:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/6eb3030775af8767891c82194e278532/href">https://medium.com/media/6eb3030775af8767891c82194e278532/href</a></iframe><h4>Öneri Motoru yazimi</h4><p>Artık sehir betimlemelerimizi temizleyip cosine similarity hesaplayan metodumuzu yazdığımıza gore, birinci versiyon icin öneri yapacak olan motorumuzu yazmaya baslayabiliriz.</p><p>İlk versiyonumuzda sadece benzerlik skoruna gore öneri yapacağımız icin motor sınıfımız küçük olacak, ama sonraki versiyonlarda ayni kodları geliştireceğimiz icin ayrı bir sınıf olarak baslamakta fayda var.</p><p>recommender_engine.py:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/3fe7ad1f1583de70c4580bdc64824044/href">https://medium.com/media/3fe7ad1f1583de70c4580bdc64824044/href</a></iframe><p>get_recommendations(keywords) metodu sırasıyla aşağıdaki gibi çalışıyor:</p><ul><li>Öncelikle, sehir betimlemelerinin benzerliğini hesaplayabileceği keywords adında String parametresi alıyor</li><li>Her sehir icin verilen keywords ile olan benzerlik skorunu hesaplıyor ve bunları sehir indexi — skor seklinde bir dictionary de tutuyor.</li><li>Sehirlerin; city, popularity, description ve score featurelarini içeren boş bir data frame oluşturuyor.</li><li>En yüksek skora sahip 5 sehri bu data frame ekliyor</li><li>Son olarak, bu data frame’i json a çevirip donuyor.</li></ul><h4>Request kodu</h4><p>Öneri yapan ve cosine similarity hesaplayan sınıflarımız olduguna göre artık bunları test etmenin zamanı geldi.<strong> request.py</strong> adında bir python dosyası oluşturalım.</p><p>Öneri uygulamamızı 3 ayrı kategori altında deneyeceğiz. Bunlar;</p><ul><li>Culture, Art and History (Kültür, Sanat ve Tarih)</li><li>Beach and Sun (Kumsal ve Güneş)</li><li>Nightlife and Party (Gece hayati ve parti)</li></ul><p>Ben veri setimizdeki sehir betimlemelerini inceleyerek, her üç kategori icin keywordleri sırasıyla aşağıdaki gibi belirledim:</p><ul><li>[history historical art architecture city culture]</li><li>[beach beaches park nature holiday sea seaside sand sunshine sun sunny]</li><li>[nightclub nightclubs nightlife bar bars pub pubs party beer]</li></ul><p>3 ayrı kategori icin request gönderecek 3 ayrı metodu request.py dosyamıza aşagıdaki gibi yazalım:</p><pre>from recommender_engine import RecommenderEngine</pre><pre>culture_keywords = &quot;history historical art architecture city culture&quot;<br>beach_n_sun_keywords = &quot;beach beaches park nature holiday sea seaside sand sunshine sun sunny&quot;<br>nightlife_keywords = &quot;nightclub nightclubs nightlife bar bars pub pubs party beer&quot;</pre><pre>def get_recommendations(keywords):<br>    result = RecommenderEngine.get_recommendations(keywords)<br>    return result</pre><pre>def get_top_5_city_names_out_of_json(json_string):<br>    list = json.loads(json_string)<br>    result = []<br>    max = len(list)<br>    i = 0<br>    while i &lt; max:<br>        result.append(list[i][&#39;city&#39;])<br>        i += 1</pre><pre>    return result</pre><pre>top_5_cultural_cities = get_recommendations(culture_keywords)<br>city_names_for_cultural = get_top_5_city_names_out_of_json(top_5_cultural_cities)<br>print(city_names_for_cultural)<br>print(&quot;#################&quot;)</pre><pre>top_5_summer_cities = get_recommendations(beach_n_sun_keywords)<br>city_names_for_summer = get_top_5_city_names_out_of_json(top_5_summer_cities)<br>print(city_names_for_summer)<br>print(&quot;#################&quot;)</pre><pre>top_5_party_cities = get_recommendations(nightlife_keywords)<br>city_names_for_party = get_top_5_city_names_out_of_json(top_5_party_cities)<br>print(city_names_for_party)<br>print(&quot;#################&quot;)</pre><p><strong>get_recommendations</strong> metodu gönderilen keywordlere gore gelen öneri json stringini donerken, <strong>get_top_5_city_names_out_of_json </strong>metodu ise donen önerilerden sehir isimlerini ve skorlarını ayrıştırıp geri donuyor. (ikinci metodun tek amacı print ettigimiz zaman sadece sehir isimlerini ve skoru görebilmek, çünkü hatırlarsanız recommender_engine her sehir icin birden farklı feature özelligi donuyor ve hepsini print ettirmek su an gereksiz.)</p><p>request.py gist:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/85604a59800970da625857c8756c71c9/href">https://medium.com/media/85604a59800970da625857c8756c71c9/href</a></iframe><p>Kodu çalıştırdığımızda 3 ayrı kategori icin önerileri ve skorlarını alacağız fakat, aşağıda sadece Kültür ve Sanat kategorisi icin olan sonuclar gösterilmektedir:</p><pre>[(&#39;Athens&#39;, 0.21629522817435007),<br> (&#39;St. Petersburg&#39;, 0.16666666666666666),<br> (&#39;Stockholm&#39;, 0.14962640041614492),<br> (&#39;Milan&#39;, 0.140028008402801),<br> (&#39;Rome&#39;, 0.12171612389003691)]</pre><p>Atina sehri icin benzerlik skoru %21,6 iken, Roma sehri icin gelen benzerlik skoru %12,2 civarinda. Benzerlik skoru beklediğinizden daha düşük gelmiş olabilir, bunun nedeni sehir betimlemelerinde doğal olarak bizim manuel olarak girdiğimiz keywordlerden farklı kelimelerin var olması. Farklı kelimeler uzayda farklı boyutların oluşmasına neden oluyor ve keywordlerimizde bu boyutlara karşılık gelen değerlerin olmaması, sonuçları düşürüyor. Eğer keywords listesine farklı kelimeler eklerseniz veya bazı kelimeleri silerseniz, sonuçların değişeceğini görebilirsiniz.</p><h4>Sonuç</h4><p>Bu versiyonda üç farklı kategori icin, seçilen kategorideki keywordler ile sehir betimlemeleri arasındaki cosine similarity skorunu hesaplayarak gezilecek sehir önerisi yapan bir öneri uygulaması geliştirdik.</p><p>Her ne kadar genel olarak benzerlik skorları düşük olsada, verilen Kültür ve Sanat kategorisi icin önerilen top 5 sehir incelendiğinde, yazdiğimiz sistemin verilen kategoriye uygun sehirler döndüğünü görebiliyoruz. Diğer kategoriler incelendiğinde gelen önerilerin verilen kategori icin mantıklı ve uygun sehirler oldugunu görülebilir. Sonuçları teyid etmek icin sehir betimlemelerini okuyabilirsiniz :)</p><p>İlk versiyonumuzun sonuna geldik. Bu versiyon için geliştirdiğimiz kodlara <a href="https://github.com/emrepun/RecommenderEngine/tree/master/version_1">şuradan</a> ulaşabilirsiniz.</p><p>Bir sonraki versiyonda cosine similarity ile birlikte farklı bir formül uygulayarak, Rating bilgisini hesaba katarak nasıl öneri yapılabileceğini inceleyeceğiz.</p><h3>Versiyon-2 (Rating Katkısı)</h3><p>Bu versiyonda veri setimizdeki <strong>rating </strong>feature’ını da kullanarak, öneri sistemimizi daha dinamik ve iyi bir hale getirmeye çalışacağız. Kötü rating’e sahip içerikleri önermek istemeyiz değil mi? :)</p><h4>CS ve rating katkısıyla skor hesaplama</h4><p>İlk olarak kaç adet rating verildiğini önemsemeyeceğiz. Bir önceki versiyonda olduğu gibi gene CS skorunu hesaplayacağız ama bu sefer ek olarak son skor hesaplamasında rating bilgisinide hesaba katacağız. Öncelikle, rating katkısını belirleyecek olan bir method yazacağız. Bu metodumuz iki parametre alacak, Q ve r. r parametresi şehir rating’i, Q parametresi ise rating katkısının son skora ne kadar etki edeceğini belirleyen bir değer olacak. Q parametresini artırıp azaltarak, rating değerinin, CS skoruna kıyasla son skora ne kadar katkı sağlayacağını belirleyeceğiz.</p><p>Yeni metodumuz son skoru, CS skoruna pozitif veya negatif katkı sağlayarak hesaplayacak. Rating katkısı eğer şehir rating’i 5&#39;in üzerindeyse pozitif (5 veya üzerinde ratinge sahip olan şehirlerin beğenildiği varsayılıyor), altında ise negatif olacak (5 altındaki şehirlerin beğenilmediği ). Rating değeri 0 ila 10 arasında değişirken, rating katkı output’u ise -Q ve +Q arasında bir değer olacak.</p><p>Örneğin Q=10 olarak verilirse, son skor, en yüksek rating için (10): CS skoru + CS skorunun % 10&#39;u olarak hesaplanırken, en düşük rating için (0): CS skoru — CS skorunun %10&#39; olarak hesaplanacak.</p><p>Metodda kullanılacak olan formül, aşağıdaki grafikte, verilen rating değerinin mavi eğride karşılık geldiği noktayı bularak bunu katkı değeri olarak dönecek. Aşağıda Q=10 için metodumuzun ne tür katkı değeri sağlayacağı görsel olarak gösterilmiştir:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ulSpIgIHHZ14Xb6QZcKW5w.png" /><figcaption>Rating katkısı hesaplayan metod (Q=10)</figcaption></figure><p>Şimdi rating_extractor.py adında bir dosya oluşturalım ve aşağıdaki kodu ekleyelim:</p><pre>class RatingExtractor:<br>    def __init__(self):<br>        print(&quot;initialized&quot;)</pre><pre>    #Returns value between -q and q. for rating input between 0 and 10.<br>    #Parameters:<br>        #rating: indicates the rating for the destination<br>        #q: indicates the percentage of rating for general score. (default is 10.)<br>    <a href="http://twitter.com/staticmethod">@staticmethod</a><br>    def get_rating_weight(rating, q=10):<br>        if rating &gt; 10 or rating &lt; 0:<br>            return None<br>        else:<br>            m = (2*q) / 10 #10 because rating varies between 0 and 10<br>            b = -q<br>            return (m*rating) + b</pre><blockquote>Metodlardaki yorumlar ingilizce olduğu için kusura bakmayın lütfen, ama zaten her metodu detaylı olarak anlatmaya çalışıyor olacağım.</blockquote><p>get_rating_weight() metodu verilen rating ve Q parametrelerine göre hesaplamalar yaparak rating katkısını hesaplayıp geri dönen bir metod. Daha öncede belirttiğim gibi, bu metod hem pozitif hem negatif değerler dönebilir. Döndüğü değere göre, son skor’a ya pozitif yada negatif bir katkı sağlıyor olacak. (Q parametresinin varsayılan değeri 10 olarak ayarlanmıştır.)</p><h4>Recommender Engine için yeni metod geliştirme</h4><p>Simdi Recommender Engine sinifimiza, cosine similarity skorunu ve rating katkisini kullanarak genel skor hesaplayacak bir method yazacagiz. Asagidaki metodu RecommenderEngine sinifina ekleyelim:</p><pre>def calculate_final_score(cs, r):<br>    amount = (cs / 100) * r</pre><pre>    return cs + amount</pre><p>Method asagidaki gibi calisiyor:</p><ul><li>CS skoru ve rating katkisi r parametrelerini aliyor</li><li>CS skorunun % +- r lik kismini amount olarak hesapliyor</li><li>Hesaplanan amount’u CS skoruna ekleyerek dönüyor.</li></ul><p>Amount pozitif veya negatif bir deger olacagi icin, son skorumuz rating katkisina bagli olarak ya CS skorunu artiracak yada azaltacak.</p><blockquote>Bu yaklaşım rating bilgisini kullanmamız için faydalı olacak fakat şu durumu belirtmekte fayda var. Rating katkısına dayalı olarak son skor hesaplaması, CS skoruna bağlı bir şekilde gerçekleşiyor. CS skorunun belirli bir yüzdesi üzerinden son skor hesaplandığı için; özellikle get_rating_weight() metodu icin yüksek Q değerleri girildiğinde, CS skoru (benzerliği) yüksek olan şehirler, düşük olan şehirlere göre daha fazla etkilenecekler.</blockquote><p>Şimdi RecommenderEngine sınıfına yeni metodlarımızı kullanarak öneri yapması için yeni bir metod yazalım. (Birinci versiyonda yazdığımız metodu hala sınıfta tutuyoruz)</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0cce3e9d21e766cc30b5e62e7c095bf2/href">https://medium.com/media/0cce3e9d21e766cc30b5e62e7c095bf2/href</a></iframe><p>get_recommendations_include_rating(keywords) metodu ilk versiyonda geliştirilen get_recommendations(keywords) metoduna benzer bir şekilde çalışacak. Fakat bu yeni metod önerileri, yeni geliştirdiğimiz metodları kullanarak, hem CS skorunu hem rating katkısını hesaba katarak yapacak. Adım adım metodumuzun nasıl çalıştığına bakalım:</p><ul><li>Keywords parametresi alıyor ve aşağıdaki işlemleri veri setindeki tüm şehirler için uyguluyor</li><li>CS skorunu hesaplıyor</li><li>Rating katkısını Q=10 olarak hesaplıyor</li><li>CS skoru ve rating katkısını kullanarak calculate_final_score methodu ile son skoru hesaplıyor</li><li>Son skora göre en yüksek skora sahip 5 şehri JSON’a çevirip dönüyor.</li></ul><h4>Request kodu</h4><p>Öncelikle RecommenderEngine’den önerileri alacak bir method yazalım:</p><pre>def get_recommendations_include_rating(keywords):<br>    return RecommenderEngine.get_recommendations_include_rating(keywords)</pre><p>Şimdi 3 kategorimiz içinde, yeni metodu kullanarak öneri alacak olan 3 farklı request yazalım:</p><pre># Version 2 requests are below:</pre><pre>top_5_cultural_with_rating = get_recommendations_include_rating(culture_keywords)<br>city_names_for_cultural_rating = get_top_5_city_names_out_of_json(top_5_cultural_with_rating)<br>print(city_names_for_cultural_rating)<br>print(&quot;#################&quot;)<br>top_5_summer_with_rating = get_recommendations_include_rating(beach_n_sun_keywords)<br>city_names_for_summer_rating = get_top_5_city_names_out_of_json(top_5_summer_with_rating)<br>print(city_names_for_summer_rating)<br>print(&quot;#################&quot;)<br>top_5_party_with_rating = get_recommendations_include_rating(nightlife_keywords)<br>city_names_for_party_rating = get_top_5_city_names_out_of_json(top_5_party_with_rating)<br>print(city_names_for_party_rating)<br>print(&quot;#################&quot;)</pre><p>Bu kod önerilen şehirleri ve son skorlarını ekrana yazdıracak, request.py çalıştırıp gelen sonuçları inceleyebilirsiniz.</p><p>Bu yazımızda sadece Kültür, Sanat ve Tarih kategorisini, iki farklı bakış açısından inceleyeceğiz. İlk olarak, bir önceki versiyonda yazdığımız sadece CS skoru ile öneri yapan metod ile, yeni yazdığımız hem CS skoru, hem rating katkısını kullanarak son skor hesaplayan metodu karşılaştıracağız.</p><h4>get_recommendations ve get_recommendations_include_rating metodlarının karşılaştırılması:</h4><p>Aşağıdaki kodu iki metodu karşılaştırma amacıyla yazdığım için request.py sınıfına dahil etmedim, dilerseniz kopyalayıp çalıştırabilirsiniz:</p><pre>top_5_cultural_cities = get_recommendations(culture_keywords)<br>city_names_for_cultural = get_top_5_city_names_out_of_json(top_5_cultural_cities)<br>print(city_names_for_cultural)<br>print(&quot;#################&quot;)</pre><pre>top_5_cultural_with_rating = get_recommendations_include_rating(culture_keywords)<br>city_names_for_cultural_rating = get_top_5_city_names_out_of_json(top_5_cultural_with_rating)<br>print(city_names_for_cultural_rating)<br>print(&quot;#################&quot;)</pre><p>İki metodun çıktısı aşağıdaki gibi:</p><pre>[(&#39;Athens&#39;, 0.21629522817435007),<br> (&#39;St. Petersburg&#39;, 0.16666666666666666),<br> (&#39;Stockholm&#39;, 0.14962640041614492),<br> (&#39;Milan&#39;, 0.140028008402801),<br> (&#39;Rome&#39;, 0.12171612389003691)]</pre><pre>#################</pre><pre>[(&#39;Athens&#39;, 0.22927294186481106),<br> (&#39;Stockholm&#39;, 0.1556114564327907),<br> (&#39;St. Petersburg&#39;, 0.15333333333333332),<br> (&#39;Milan&#39;, 0.15123024907502508),<br> (&#39;Rome&#39;, 0.13145341380123987)]</pre><p>Yukarıda, her iki farklı içinde farklı skor ve şehir sıralamaları verilmiştir. Gördüğünüz gibi, yeni geliştirdiğimiz metodda (alttaki) Stockholm ikinci sıraya yükselirken, St. Petersburg üçüncü sıraya geriledi. Gelin bunun nedenini inceleyelim:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xlFOpd_qANbMEgu8iFAnSQ.png" /></figure><p>Veri setimizde görüldüğü üzere, Stockholm’ün rating’i 7 iken, St. Petersburg’un ratingi 1. Bu yüzden algoritmamız St. Petersburg için son skoru, Cs skoruna göre daha düşük hesaplarken, Stockholm için ise son skoru daha yüksek hesaplıyor. Bu yüzden son skorda Stockholm, St. Petersburg’u geçerek ikinci sıraya yükseliyor. Burada görebiliyoruzki, yazdığımız metod ve formüller, yüksek ratingli içeriklerin skorunu artırırken, düşük ratingli içeriklerin skorunu azaltıyor. Veri setimizdeki diğer şehirlerin rating bilgisini de inceleyerek, genel olarak skorlardaki değişimlerin nedenlerini gözlemleyebilirsiniz.</p><h4>get_recommendations_include_rating metodunun Q = 10 ve Q = 100 için karşılaştırılması:</h4><p>Şimdi yeni metodumuzu farklı Q parametreleriyle karşılaştıracağız. Hatırlarsanız, rating katkısı, Q parametresinin değeriyle doğru orantılı olarak değişiyor. Bir önceki karşılaştırmada yazdırdığımız gibi, Q=10 için, Kültür, Sanat ve Tarih kategorisinde son skor hesaplamasının en yüksek skorlu 5 şehri:</p><pre>[(&#39;Athens&#39;, 0.22927294186481106),<br> (&#39;Stockholm&#39;, 0.1556114564327907),<br> (&#39;St. Petersburg&#39;, 0.15333333333333332),<br> (&#39;Milan&#39;, 0.15123024907502508),<br> (&#39;Rome&#39;, 0.13145341380123987)]</pre><p>Şimdi Q parametresini 100 yaparak sonuçları inceleyeceğiz. recommender_engine.py dosyasına gidip get_recommendations_include_rating metodundaki 10 sayısını 100 olarak güncelleyerek parametre değerini artırabilirsiniz:</p><pre>rating_contribution = RatingExtractor.get_rating_weight(rating,100)</pre><p>Şimdi yeni sonuçlarımıza bakalım:</p><pre>[(&#39;Athens&#39;, 0.3460723650789601),<br> (&#39;Milan&#39;, 0.2520504151250418),<br> (&#39;Rome&#39;, 0.21908902300206645),<br> (&#39;Stockholm&#39;, 0.2094769605826029),<br> (&#39;Venice&#39;, 0.17777777777777776)]</pre><p>Q parametresini 100 yaptığımızda, sonuçlarımızın çok daha farklı olduğunu görebiliyoruz:</p><ul><li>St. Petersburg şehri artık ilk 5 de bile değil, 1 ratingi olduğu için Q parametresi de yükselince, son skoru tamamen düşük bir değer aldı.</li><li>Stockholm dördüncü sıraya düşerken, Milan ve Roma, ikince ve üçüncü sıralara yükseldi, aşağıda görülebileceği gibi Milan ve Romanın rating’i Stockholm’e göre daha yüksek olduğu için</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*L6b7lPX8dTSpAkfAzOZ0BA.png" /><figcaption>Roma, Milan ve Stockholm için rating kıyaslaması</figcaption></figure><p>Diğer şehir kategorileri içinde farklı Q parametreleriyle sonuçların nasıl değiştiğini incelemenizi öneririm.</p><h4>Versiyon-2 Sonucu</h4><p>İkinci versiyonumuzda Cosine benzerliğinin yanı sıra rating bilgisini de kullanarak şehir önerisi yapan yeni bir method geliştirdik. Öneri sistemlerinde rating gibi bilgileri kullanmak oldukça önemli, çünkü genellikle insanlar tarafından beğenilen içerikleri önermek isteriz.</p><p>İkinci versiyonun tüm kodlarına <a href="https://github.com/emrepun/RecommenderEngine/tree/master/version_2">buradan</a> ulaşabilirsiniz.</p><p>Bir sonraki versiyonda verilen rating sayısınıda hesaba katarak öneri sistemimizi daha iyi bir hale getirmeye çalışacağız.</p><h3>Versiyon-3 (Rating Eşik Değeri)</h3><p>Bir içeriğin yüksek rating’e sahip olması, bu rating’in güvenilir olduğu anlamına gelmez. A ve B adında iki farklı içerik olduğunu düşünün. A, 500.000 kişinin verdiği rating sonucunda 4.7 rating’e sahip ve B ise 10 kişinin verdiği rating sonucunda 5 ratingi’ne sahip. Hangi içeriği bir arkadaşınıza önermek isterdiniz? B içeriğinin sahip olduğu 5 ratingi, sadece 10 kişinin rating verdiği göz önüne alındığında ne kadar güvenilir olabilir? rating_count feature’ı sayesinde; ratingler için bir eşik parametresi oluşturacağız ve öneri sistemimiz, rating sayısı bu parametreden düşük olan içeriklere (şehirler) rating katkısı hesaplanırken çok fazla ağırlık yüklemeyecek. Bu sayede az sayıda kişiden rating almış şehirlerin rating bilgisi pek fazla kaale alınmamış olacak.</p><h4>Rating count feature’ı ile rating ağırlık hesaplanması</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/530/1*IwRNiI8ZFZdmkaJGSXrk7A.png" /><figcaption>Multiplier formülü</figcaption></figure><p>Yukarıdaki formül sayesinde hesaplanan M çarpanı, rating katkısı ile çarpılarak, son öneri skoru hesaplanmasında kullanılmak üzere rating ağırlığını elde etmemize yardımcı olacak. T eşik sayısını, c ise bir şehrin aldığı rating sayısını temsil ediyor. Bu formül aşağıdaki gibi çalışacak şekilde tasarlanmıştır:</p><ul><li>M değeri 0.0 ve 1.0 arasında değişebilir</li><li>T ve c parametreleri eşit olduğu durumda, M her zaman 0.50 ye eşittir.</li></ul><blockquote>Bu formülde e sayısının kullanılması için herhangi bir özel durum bulunmamakta, bir başka sayı kullanarakta formülü oluşturabilirdik (o zaman 0,68 sayısı değişmek zorunda kalırdı). Havalı görünmesi için e sayısını kullandım :P</blockquote><p>Bu formülün en önemli noktası, T (eşik) ve c (rating sayısı) değerleri birbirine eşit olduğunda 0.50 değerini veriyor olması. Bir şehrin aldığı rating sayısı bizim girdiğimiz eşik değerinden düşükse, M değeri 0.0–0.50 aralığında, eğer eşik değerinden yüksekse, M değeri 0.50–1.0 arasında olacak. Ama rating sayısı ve eşik değeri ne olursa olsun, M değeri asla 1.0 dan yüksek olamayacak.</p><p>Şimdi rating_extractor.py dosyasına gidelim ve yeni bir metod yazalım. Burada yalnızca rating katkısını M ile çarpacağız fakat, daha önce geliştirdiğimiz metoduda ilerde olduğu gibi kullanabilmeniz adına bu yeni yazacağımız metodu ayrı olarak yazacağız.</p><p>İlk olarak e’yi dosyamıza import edeceğiz:</p><pre>from math import e</pre><p>Sonra, RatingExtractor sınıfına aşağıdaki metodu ekleyelim:</p><pre><a href="http://twitter.com/staticmethod">@staticmethod</a><br>def get_rating_weight_with_quantity(rating, c, T, q=10):<br>    if rating &gt; 10 or rating &lt; 0:<br>        return None<br>    else:<br>        m = (2*q) / 10 #10 because rating varies between 0 and 10<br>        b = -q<br>        val = (m*rating) + b</pre><pre>        M = e**((-T*0.68)/c)</pre><pre>        return val * M</pre><p>Metod aşağıdaki gibi çalışacak:</p><ul><li>rating, c (rating sayısı), T (eşik) ve Q parametrelerini alıyor.</li><li>rating ve Q parametrelerini önceki bölümlerde görmüştük.</li><li>rating katkısını hesaplıyor</li><li>Verilen parametrelere göre M çarpanını hesaplıyor</li><li>rating katkısını M ile çarparak rating ağırlığını dönüyor.</li></ul><h4>RecommenderEngine sınıfında yeni metod geliştirme</h4><p>Şimdi recommender_engine.py dosyasını açalım ve RecommenderEngine sınıfına yeni bir metod ekleyelim (önceki bölümlerde geliştirdiğimiz metodları hala tutuyoruz). Bu ekleyeceğimiz metod aslında önceki bölümlerde geliştirdiğimiz metodlara oldukça benziyor fakat bu sefer, şehir betimlemesi ve rating ile birlikte, rating sayısı ve T (eşik değeri) parametrelerini kullanacağız.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/961233d0c30c17be6c90b2cde739acbf/href">https://medium.com/media/961233d0c30c17be6c90b2cde739acbf/href</a></iframe><p>Metodumuz aşağıdaki gibi çalışıyor:</p><ul><li>Keywords parametresi alıyor, ve veri setindeki tüm şehirler için aşağıdaki adımları gerçekleştiriyor</li><li>CS skorunu hesaplıyor.</li><li>Her şehir için betimleme, rating, rating sayısı, eşik T = 1.000.000 (Veri setimizde rating sayısı 100 bin ila 5 milyon arasında değiştiği için 1 milyon değerini seçtim) ve Q=10 değerleriyle rating ağırlığını hesaplıyor.</li><li>CS skoru ve rating ağırlığıyla calculate_final_score metodunu (önceki bölümde geliştirilmişti) çağırarak, son skoru hesaplıyor.</li><li>En yüksek skora sahip 5 şehri JSON’a çevirip dönüyor.</li></ul><h4>Request kodu</h4><p>Sırada request.py dosyasına yeni metodumuzu kullanarak 3 farklı kategori için request göndermek var.</p><p>Öncelikle, yeni metodumuzu kullanarak önerileri alacak bir metod yazalım:</p><pre>def get_recommendations_include_rating_count_threshold(keywords):<br>    return RecommenderEngine.get_recommendations_include_rating_count_threshold(keywords)</pre><p>Şimdi 3 kategori için önerileri alacak olan 3 request yapalım:</p><pre># Version 3 requests are below:</pre><pre>top_5_cultural_with_rating_count_threshold = get_recommendations_include_rating_count_threshold(culture_keywords)<br>city_names_for_cultural_rating_count_threshold = get_top_5_city_names_out_of_json(top_5_cultural_with_rating_count_threshold)<br>print(city_names_for_cultural_rating_count_threshold)<br>print(&quot;#################&quot;)</pre><pre>top_5_summer_with_rating_count_threshold = get_recommendations_include_rating_count_threshold(beach_n_sun_keywords)<br>city_names_for_summer_rating_count_threshold = get_top_5_city_names_out_of_json(top_5_summer_with_rating_count_threshold)<br>print(city_names_for_summer_rating_count_threshold)<br>print(&quot;#################&quot;)</pre><pre>top_5_party_with_rating_count_threshold = get_recommendations_include_rating_count_threshold(nightlife_keywords)<br>city_names_for_party_rating_count_threshold = get_top_5_city_names_out_of_json(top_5_party_with_rating_count_threshold)<br>print(city_names_for_party_rating_count_threshold)<br>print(&quot;#################&quot;)</pre><p>Yukardaki kod bloğu, 3 kategori içinde son skorlarına bağlı olarak önerileri yazdıracak. request.py çalıştırarak tüm kategoriler için sonuçları görebilirsiniz. Ama biz bu yazımızda yalnızca Kültür, Sanat ve Tarih kategorisi için sonuçları inceleyeceğiz.</p><h4>Farklı eşik değerleriyle sonuçların kıyaslaması</h4><p>Gelin, Kültür Sanat ve Tarih kategorisi için farklı T değerleriyle deneysel requestler yapalım. Threshold değerini RecommenderEngine sınıfındaki get_recommendations_inçlude_rating_count_threshold metodundan değiştirebilirsiniz. Ayrıca eşik etkisini daha iyi görebilmek için Q değerinide 100&#39;e arttıralım (önceki bölümlerde anlatıldığı gibi Q yükseldikçe son skor hesaplamasında rating’in etkisi CS skoruna oranla artıyor).</p><p>T = 100.000:</p><pre>[(&#39;Athens&#39;, 0.33318171469723395),<br> (&#39;Milan&#39;, 0.24587898720843948),<br> (&#39;Rome&#39;, 0.21192640793273687),<br> (&#39;Stockholm&#39;, 0.18358642633975064),<br> (&#39;Venice&#39;, 0.17262307588744202)]</pre><p>T = 1.000.000:</p><pre>[(&#39;Athens&#39;, 0.26188415260156817), <br>(&#39;Milan&#39;, 0.2035910531885378), <br>(&#39;Rome&#39;, 0.16707033294390228), <br>(&#39;Stockholm&#39;, 0.14983344608755947), <br>(&#39;Barcelona&#39;, 0.14757848986361075)]</pre><p>T = 2.500.000:</p><pre>[(&#39;Athens&#39;, 0.2257870828894539), <br>(&#39;Milan&#39;, 0.16719580286435054), <br>(&#39;St. Petersburg&#39;, 0.158824470447676), <br>(&#39;Stockholm&#39;, 0.14962644254339), <br>(&#39;Rome&#39;, 0.13613352041126298)]</pre><p>Yukardaki sonuçlardan görüldüğü üzere, önerilerdeki 5. sıradaki şehir, eşik değeri 100 bin ve 1 milyon olmasına göre değişiyor. Eşik değeri düşük olduğunda beşinci şehir Venedik iken, değer yüksek olduğunda beşinci şehir Barselona. Bunun nedenini görelim:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/408/1*z-Q5cyg0v6v__mElMDAC0w.png" /></figure><p>İki şehrin ratingi de 8 ama, Barselona daha fazla rating sayısına sahip, ayrıca Venedik, Barselonaya kıyasla daha fazla CS skoruna sahip. Bu yüzden eşik değeri 100.000 olduğunda, iki şehirde iyi miktarda rating katkı puanına sahip ve Venediğin CS skoru daha yüksek olduğundan, beşinci sırada Venediği görüyoruz.</p><p>Ama eşik değeri 1.000.000 olduğunda, rating katkı skorları aşağıdaki gibi hesaplanıyor (Q=100):</p><ul><li>Barcelona: 34</li><li>Venice: 26.8</li></ul><p>Barselona daha fazla rating katkı puanına sahip ve Q değeride yüksek olduğu için, son skor hesaplandığında elde edilen değer Barselona için daha yüksek olduğundan, beşinci sırada Barselonayı görüyoruz.</p><p>Eşik değeri 2.500.000 olduğunda St. Petersburg’u 3. sırada görüyoruz. Ama daha düşük eşik değerlerinde St.Petersburg’u 4. yada 5. sırada bile göremiyorduk. Bunun nedenini araştırmayı sizlere bırakacağım. St. Petersburg şehri için veri setini inceleyip, yazdığımız metodların üzerinden tekrar geçerek, bunun nedenini anlamaya çalışın. Eğer bir sorunuz olursa bana sorabilirsiniz. :)</p><p>Bunun haricinde, parametre değerlerini değiştirip farklı değerlerle oynamanızı, veri setindeki değerleri incelemenizi ve aldığınız sonuçları tüm kategoriler içinde inceleyerek, yazdığımız metodların nasıl çalıştığını ve bu metodların öneri sistemlerindeki değerini anlamaya çalışmanızı öneririm.</p><h4>Versiyon-3 Sonucu</h4><p>Üçüncü versiyonda, öncelikle şehirlerin CS skorunu ve rating ve rating sayısı featurelarını kullanarak rating ağırlığını hesaplayan, daha sonra CS skoru ve rating ağırlığını kullanarak son skor hesaplayarak şehir öneren bir metod geliştirdik. Öneri sistemlerinde verinin güvenilirliği göz önünde bulunarak öneri yapılması gerektiğinden, yazdığımız metod sayesinde öneri sistemimizin nasıl daha yüksek sayıda (yüksek sayı uygulamadan uygulamaya değişiklik gösterecektir) feedback içeren içerikleri teşvik edebileceğini gördük.</p><p>Yazılan tüm kodlara <a href="https://github.com/emrepun/RecommenderEngine/tree/master/version_3">buradan</a> erişebilirsiniz.</p><p>Bir sonraki versiyonda, farklı türden feedbackleri işleyerek öneri yapan bir metod geliştireceğiz.</p><h3>Versiyon-4</h3><p>Bu versiyonda <strong>positive_review</strong> ve <strong>negative_review</strong> featurelarını da kullanarak öneri sistemimizi geliştirmeye devam edeceğiz.</p><blockquote>Bu bölümde önceki bölümlere kıyasla, daha çok teori ve deneysel sonuçlar hakkında konuşacağız, eğer sadece kod ile ilgileniyorsanız. <strong>Uygulama</strong> kısmından başlayabilirsiniz.</blockquote><p>Bazen uygulamalarımızda içeriklerimiz için birden farklı tipte feedback’e sahip olduğumuz durumlar olabilir, review ve rating gibi. Tahmin edebileceğiniz gibi, bu feedbackler aynı türde değil, rating feedback’i belirli sayısal bir aralık üzerinde (bizim uygulamamızda 0–10 arası) verilirken review feedback’i genellikle metin olarak verilir. Sahip olduğumuz reviewleri pozitif ve negatif olarak sınıflandırdığımız hayal edelim (belki verilen reviewleri pozitif/negatif olarak sınıflandırma üzerinde farklı bir yazı yazabiliriz), o zaman review feedback’i binary feedback (0 yada 1) olarak ele alınabilir.</p><p>Veri setimizde her şehir için positive_review ve negative_review adları altında her şehrin aldığı pozitif ve negatif review sayılarını gösteren featurelar mevcut.</p><p>Öneri uygulamalarının başa çıkması gerektiği sorunlardan biride farklı türden feedbackleri birlikte kullanarak daha anlamlı öneriler yapabilme. Bunu yapmak için farklı yöntemler mevcut olsada biz bu yazımızda, özel bir yöntemle alınan reviewleri rating’e dönüştürerek farklı türdeki feedbackleri bir arada kullanacağız.</p><h4>Reviewleri ratinge çevirme</h4><p>En basit yöntemle bir review’i ratinge dönüştürmek, pozitif ve negatif reviewler için belirli birer rating değeri belirlemek olurdu. Reviewler bu rating değeriyle dönüştürüldükten sonra, ortalama rating, asıl rating ve dönüştürülen rating hesaba katılarak tekrardan hesaplanabilirdi. Ama bu yaklaşım pek iyi bir yaklaşım olmazdı. Örneğin, ratingler 0 ve 10 olarak seçilmiş olsa, reviewlerin etkisi içerikler üzerinde çok fazla olurdu (özellikle bir içeriğin ortalama ratingi zaten 0 veya 10 a yakınsa). Bu ekstrem etkiyi azaltmak için farklı rating değerleri seçilebilirdi, eğer ratingler 2.5 ve 7.5 olarak seçilse, bu seferde farklı bir problem ortaya çıkacaktı. Ortalama değeri 7.5&#39;in üzerinde olan bir içerik için, otomatik olarak 7.5 ratingine dönüştürülmüş pozitif bir review, pozitif olmasına rağmen, içeriğin ortalama ratinginden düşük olduğu için negatif bir etki oluştururdu. Aynı şekilde hali hazırda rating ortalaması 2.5&#39;in altında olan içerikler içinde, otomatik olarak 2.5 değerine dönüştürülen negatif reviewler, pozitif etki gösterebilirdi. Bu sebeplerden dolayı, daha iyi bir method geliştirmekte fayda var.</p><p>Geliştireceğimiz method pozitif ve negatif reviewler için sırasıyla aşağıdaki gibi davranacak:</p><ul><li>Her bir pozitif review için, içeriğin ortalama rating’i ile rating skalasında alınabilecek en yüksek değer (bizim uygulamamız için 10) arasındaki mesafe hesaplanıp sonra bu mesafenin yarısı, içeriğin ortalama ratingine <strong>eklenerek</strong>, pozitif bir review ratinge çevrilmiş olacak.</li><li>Her bir negatif review için, içeriğin ortalama rating’i ile rating skalasında alınabilecek en düşük değer (bizim uygulamamız için 0) arasındaki mesafe hesaplanıp, sonra bu mesafenin yarısı içeriğin ortalama ratinginden <strong>çıkarılarak</strong>, negatif bir review ratinge çevrilmiş olacak.</li></ul><p>Pozitif ve negatif reviewleri ratinge çevrilmek için kullanılacak formüller Rp ve Rn olarak sırasıyla aşağıda verilmiştir (r içeriğin ortalama rating değeri):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/370/1*avtIfugLIW6_CwJxOyPlLg.png" /><figcaption>Pozitif bir review için rating değer dönüşümü</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/350/1*b_RnbZsY0gYa-fulKfg39g.png" /><figcaption>Negatif bir review için rating değer dönüşümü</figcaption></figure><p>Örneğin, ortalama ratingi 6 olan bir içeriğe verilen her bir negatif review 3 değeriyle ratinge çevirilerek ratinglerin arasına eklenirken, her bir pozitif review 8 değeriyle çevrilere eklenecek. Sonra ortalama değer skor hesaplamasında kullanılmadan önce, içeriğin hali hazırda sahip olduğu ratingler + yeni dönüştürülen ratingler hesaba katılarak tekrardan hesaplanacak. Review feedback dönüşümü sonuçları farklı rating, rating sayısı ve review sayıları için aşağıdaki tabloda verilmiştir. (tabloyu ingilizce olarak hazırladığım seriden aldığım için sütun başlıkları türkçe değil maalesef)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*thRMbk5DHyloHa6e0uYOGw.png" /><figcaption>Ortalama rating ve reviewler için rating değer hesaplaması</figcaption></figure><p>Reviewler hesaba katıldıktan sonra tekrar hesaplanan rating değerine bakıldığında, içeriğin ratingi alınabilecek en yüksek ratinge yakın olduğunda ve pozitif ve negatif review sayıları arasında çok fark olmadığında, pozitif review, negatif reviewden fazla olmasına rağmen, metodumuz negatif bir etkiye sahip oluyor. Aynı şekilde içeriğin ratingi alınabilecek en düşük ratinge yakın olduğunda ve negatif ve pozitif review sayıları arasında çok fark olmadığında, negatif review pozitif reviewden fazla olmasına rağmen, metodumuz pozitif bir etkiye sahip oluyor. Örneğin ortalama rating 7,2 olduğunda ve negatif, pozitif review sayıları eşit olduğunda, sonucun 6,65 olduğunu görüyoruz. Bunun nedeni 0 ila 7,2 arasındaki mesafenin 7,2 ila 10 arasındaki mesafeden fazla olması. Bu yüzden hesaplanan değer daha negatif bir etki yaratıyor. Ama genellikle uç kısımlara yakın ratinge sahip içerikler için pozitif ve negatif review sayıları birbirine yakın olmadığı için, bu sorun sistemimizi çokta kötü etkilemeyebilir. Dahası, genellikle içerikler reviewe kıyasla daha çok rating aldığı için, yukardaki testlerdede sayılar buna göre verildi ve haliyle reviewlerin etkisi çok fazla değil. Bu etki farklı bir parametre ekleyerek artırılabilirdi. (Örneğin her bir pozitif review için hesaplanan rating değerine sahip 10 adet rating ekle şeklinde. Şu anda biz her bir review için 1 adet rating ekliyoruz.)</p><h4>Uygulama</h4><p>Artık metodumuzun nasıl çalıştığını ve ne tür sonuçlar verdiğini gördüğümüze göre, rating_extactor.py dosyasını açalım ve RatingExtractor sınıfına aşağıda metodu ekleyelim:</p><pre><a href="http://twitter.com/staticmethod">    @staticmethod</a><br>    def get_rating_with_count_and_reviews(r, rc, pf, bf):<br>        if r &gt; 10 or r &lt; 0:<br>            return None<br>        else:<br>            positive_diff = (10 - r) / 2<br>            positive_rating = r + positive_diff</pre><pre>            negative_diff = r / 2<br>            negative_rating = r - negative_diff</pre><pre>            updated_rating = ((r * rc) + (pf * positive_rating) + (bf * negative_rating)) / (rc + pf + bf)</pre><pre>return RatingExtractor.get_rating_weight_with_quantity(updated_rating,rc,1000000,10)</pre><p>Metod aşağıdaki şekilde çalışacak:</p><ul><li>r (rating), rc (rating sayısı), pf (pozitif review sayısı) ve bf (negatif review sayısı parametrelerini alıyor.</li><li>Pozitif reviewler için dönüştürülen rating değerini hesaplıyor.</li><li>Negatif reviewler için dönüştürülen rating değerini hesaplıyor.</li><li>Güncellenen rating değerini, eski ortalama rating ve yeni dönüştürülen ratingleri hesaba katarak hesaplıyor.</li><li>Daha önceki bölümde geliştirilen bir metodu, güncellenen rating değeri, rating sayısı, T = 1.000.000 (eşik) ve Q = 100 (rating önem parametresi) parametreleriyle çağırarak sonucu rating katkısı olarak dönüyor.</li></ul><h4>RecommenderEngine sınıfına yeni metod ekleme</h4><p>Şimdi, recommender_engine.py dosyasını açalım ve aşağıdaki metodu RecommenderEngine sınıfına ekleyelim. Bu metod daha önce geliştirdiğimiz metodlara benzemesine rağmen, şimdi pozitif review ve negatif review sayılarını da kullanacağız.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/aa32be4d26c0376de71380e2718a7435/href">https://medium.com/media/aa32be4d26c0376de71380e2718a7435/href</a></iframe><p>Metod aşağıdaki gibi çalışacak:</p><ul><li>Keywords parametresini alıyor ve aşağıdaki adımları şehirler için uyguluyor</li><li>CS skorunu hesaplıyor</li><li>rating, rating count, positive review count ve negative review count parametrelerini kullanarak, rating katkı ağırlığı değerini hesaplıyor. Bu sefer T ve Q parametreleri direk RatingExtractor sınıfındaki yeni metodda kullanıldı.</li><li>calculate_final_score metodunu (daha önceki bölümlerde geliştirildi) CS skoru ve rating ağırlığı parametreleriyle çağırarak son skoru hesaplıyor.</li><li>En yüksek skora sahip 5 şehri JSON a çevirerek dönüyor.</li></ul><h4>Request</h4><p>request.py dosyasına, her üç kategori için öneri alacak requestler ekleyeceğiz.</p><p>Öncelikle, RecommenderEngine sınıfındaki yeni metodla önerileri alacak bir metod yazalım:</p><pre>def get_recommendations_include_rating_count_threshold_positive_negative_reviews(keywords):<br>    return RecommenderEngine.get_recommendations_include_rating_count_threshold_positive_negative_reviews(keywords)</pre><p>Şimdi yeni metodu kullanarak her 3 kategori için önerileri alalım ve ekrana yazdıralım:</p><pre># Version 4 requests are below:</pre><pre>top_5_cultural_with_rating_count_threshold_reviews = get_recommendations_include_rating_count_threshold_positive_negative_reviews(culture_keywords)<br>city_names_for_cultural_rating_count_threshold_reviews = get_top_5_city_names_out_of_json(top_5_cultural_with_rating_count_threshold_reviews)<br>print(city_names_for_cultural_rating_count_threshold_reviews)<br>print(&quot;#################&quot;)</pre><pre>top_5_summer_with_rating_count_threshold_reviews = get_recommendations_include_rating_count_threshold_positive_negative_reviews(beach_n_sun_keywords)<br>city_names_for_summer_rating_count_threshold_reviews = get_top_5_city_names_out_of_json(top_5_summer_with_rating_count_threshold_reviews)<br>print(city_names_for_summer_rating_count_threshold_reviews)<br>print(&quot;#################&quot;)</pre><pre>top_5_party_with_rating_count_threshold_reviews = get_recommendations_include_rating_count_threshold_positive_negative_reviews(nightlife_keywords)<br>city_names_for_party_rating_count_threshold_reviews = get_top_5_city_names_out_of_json(top_5_party_with_rating_count_threshold_reviews)<br>print(city_names_for_party_rating_count_threshold_reviews)<br>print(&quot;#################&quot;)</pre><p>Yukardaki kod, her 3 kategori içinde sonuçları alacak ve ekrana yazdıracak. request.py dosyasını çalıştırarak aldığınız sonuçları görebilir ve inceleyebilirsiniz. Yazımızın başında metodumuz ve alınabilecek sonuçlar detaylı olarak incelendiği için bu bölümde kod sonucu inceleme kısmını pas geçeceğim. Yalnızca Kültür, Sanat ve Tarih kategorisi için alınan sonuçlar aşağıda verilmiştir:</p><pre>[(&#39;Athens&#39;, 0.2622560540924768), <br>(&#39;Milan&#39;, 0.2040068651858985), <br>(&#39;Rome&#39;, 0.16752794267650856), <br>(&#39;Stockholm&#39;, 0.14984473241175314), <br>(&#39;Barcelona&#39;, 0.14831614523091158)]</pre><p>Ama tüm kategoriler için alınan sonuçları detaylı bir şekilde incelemenizi öneririm. Veri setini inceleyerek 3. versiyondaki sonuçlarla bu versiyonda aldığımız sonuçları kıyaslamanız kesinlikle faydalı olacaktır. Nasıl bir fark görüyorsunuz ve sizce neden? Eğer bir sorunuz olursa bana yorumlarda sorabilirsiniz :)</p><h4>Sonuç</h4><p>Bu versiyonda Öneri Sistemlerinde farklı türden feedbackleri birbirine dönüştürerek birlikte kullanmayı gördük. Farklı yöntemlerin nasıl negatif etkileri olabileceğini, ve geliştirdiğimiz sisteminde bazı durumlarda nasıl istenmeyen sonuçlar doğurabildiğini inceledik. Siz olsaydınız nasıl bir formülle bu sorunları atlatmaya çalışırdınız? Bunu düşünmeniz faydalı olacaktır.</p><p>Bu versiyonla birlikte, Öneri Sistemi (Recommender System) uygulamamızın sonuna geldik. Bu yazımızda, kullanıcı hakkında hemen hemen hiçbir şey bilmediğimiz durumlarda (cold start problem), kullanıcıların sadece belirli bir kategori seçmesini isteyerek farklı tekniklerle öneri yapan bir sistem geliştirdik. Umarım sonuna kadar takip edip bu yazıdan memnun kalmışsınızdır. Bu yazıda geliştirilen formül ve teknikler benim kendi fikirlerim olduğu için mükemmel değiller, buna yazılarımızdada zaman zaman şahit olduk veya değindik. Bu yüzden eğer sistemi dahada geliştirebileceğimizi düşündüğünüz alanlar varsa, lütfen yorum olarak belirtin :)</p><p>Projemizin son halini <a href="https://github.com/emrepun/RecommenderEngine/tree/master/final_version">buradan</a> indirebilirsiniz.</p><p>Hoşçakalın.</p><h4>Ekstra</h4><p>Veri setimizde olan bazı featureların neden kullanılmadığını merak ediyor olabilirsiniz. Birinci bölümün başında belirttiğim gibi, bu serimizde geliştirilen teknikler benim master projemin bir parçası. Master projemin bir diğer parçasıda Flutter kullanarak cross platform mobil uygulama geliştirmekti. Bazı featureları orada kullanmıştım. Bahsettiğim uygulamanın ekran görüntülerini aşağıda görebilirsiniz:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DXtpR3S5_wiTf2HfpOO4ng.png" /><figcaption>Mobil uygulamadan resimler</figcaption></figure><p>Eğer bu serimizde geliştirdiğimiz sistem için Flutter kullanarak UI geliştirmek ilginizi çektiyse, lütfen beni haberdar edin. Belki başka bir yazıda birlikte uygulama geliştirebiliriz :)</p><p>Kendinize iyi bakın!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1323e3b5b6ae" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>