Skip to content

Commit

Permalink
Add Lab 08 assignment
Browse files Browse the repository at this point in the history
  • Loading branch information
100yo committed Nov 30, 2024
1 parent d0fd9c7 commit ba181ea
Show file tree
Hide file tree
Showing 6 changed files with 2,800 additions and 0 deletions.
287 changes: 287 additions & 0 deletions 08-lambdas-and-stream-api/lab/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
# Fraud Detector :credit_card: :male_detective:

В наши дни, електронните разплащания са гръбнакът на световната финансова система. Все по-често предпочитаме да пазаруваме онлайн, да плащаме с карта и да превеждаме пари по електронен път. Електронните парични потоци за съжаление все по-често стават и обект на посегателство от злонамерени агенти, които извършват различни видове измами, възползвайки се от пробиви в сигурността, кражба на лични данни, социално инженерство и други. Това е актуален и значим проблем, както за финансовите институции, така и за индивидуалните потребители като нас. По различни оценки, загубите от измами възлизат на около 5% от световния финансов оборот: повече от 6 трилиона долара годишно!

Това поражда необходимостта от разработването на сложни системи и алгоритми за откриване на измами, които надеждно да гарантират сигурността и доверието в електронните финансовите операции.

Откриването на измами (*fraud detection*) е свързано с анализ на риска и идентифицирането на *подозрителни* финансови транзакции: това са транзакции, чиито характеристики ги определят като "отклонения" (*outliers*), т.е. като значително различаващи се от останалите транзакции в един поток или набор от данни. Тези отклонения (*аномалии*) се откриват автиматично и задействат детайлното им разглеждане от човек.

Тази седмица ще приложим знанията си за работа с файлове, входно-изходни потоци, ламбда изрази и Java Stream API-то, за да създадем **Fraud Detector**, който ще изчислява риска, анализирайки финансови транзакции на потребители (акаунти). Анализът ще се базира на реален *dataset*, съдържащ данни за над 2,500 финансови транзакции, който е взет от [kaggle](https://www.kaggle.com/datasets/valakhorasani/bank-transaction-dataset-for-fraud-detection), онлайн платформата за machine learning и data science на Google. Понеже оригиналният dataset съдържа някои колони, които няма да ни трябват за задачата, сме ги премахнали, като може да свалите обработения файл от [тук](./resources/dataset.csv).

Всеки ред от файла съдържа информация за една транзакция, като полетата са разделени със запетая (CSV формат):

`TransactionID,AccountID,TransactionAmount,TransactionDate,Location,Channel`

## Transaction Analyzer

В пакета `bg.sofia.uni.fmi.frauddetector.analyzer` създайте клас `TransactionAnalyzerImpl`, който има публичен конструктор `TransactionAnalyzerImpl(Reader reader, List<Rule> rules)` и имплементира интерфейса `TransactionAnalyzer`:

```java
package bg.sofia.uni.fmi.frauddetector.analyzer;

import bg.sofia.uni.fmi.frauddetector.transaction.Channel;
import bg.sofia.uni.fmi.frauddetector.transaction.Transaction;

import java.util.List;
import java.util.Map;
import java.util.SortedMap;

public interface TransactionAnalyzer {

/**
* Retrieves all transactions loaded into the analyzer.
*
* @return a list of all transactions.
*/
List<Transaction> allTransactions();

/**
* Retrieves all unique account IDs from the loaded transactions.
*
* @return a list of unique account IDs as strings.
*/
List<String> allAccountIDs();

/**
* Retrieves a map of transaction counts grouped by the channel of the transaction.
*
* @return a map where keys are Channel values and values are the count of transactions for each channel.
*/
Map<Channel, Integer> transactionCountByChannel();

/**
* Calculates the total amount spent by a specific user, identified by their account ID.
*
* @param accountID the account ID for which the total amount spent is calculated.
* @return the total amount spent by the user as a double.
* @throws IllegalArgumentException if the accountID is null or empty.
*/
double amountSpentByUser(String accountID);

/**
* Retrieves all transactions associated with a specific account ID.
*
* @param accountId the account ID for which transactions are retrieved.
* @return a list of Transaction objects associated with the specified account.
* @throws IllegalArgumentException if the account ID is null or empty.
*/
List<Transaction> allTransactionsByUser(String accountId);

/**
* Calculates the risk score for each account based on the loaded rules.
* The score for each account is a double-precision floating-point number in the interval [0, 1]
* and is formed by summing up the weights of all applicable rules to the account transactions.
* The result is sorted in descending order, with the highest-risk accounts listed first.
*
* @return a map where keys are account IDs (strings) and values are risk scores (doubles).
*/
SortedMap<String, Double> accountsRisk();

}
```

На конструктора се подава коректно създаден входен поток, от който може да се изчете dataset-a.
При извикване на конструктора с множество правила със сумарна тежест, различна от 1.0, хвърляйте `IllegalArgumentException`.

:point_right: Обърнете внимание, че dataset-ът съдържа заглавен ред, който трябва да пропуснете при изчитането му.

:point_right: Напомняне: когато сравнявате числа с плаваща запетая, не разчитайте на точно равенство, а работете с равенство с определена точност (делта).

### Транзакции

Транзакциите се моделират от record-a `Transaction`, който има следните компоненти: `(String transactionID, String accountID, double transactionAmount, LocalDateTime transactionDate, String location, Channel channel)` и има публичен статичен factory метод със сигнатура

`public static Transaction of(String line)`,

който по даден низ, представляващ ред от dataset-a, връща `Transaction` обект.

:point_right: Подсказка: за парсването на датата, ще ви влезе в употреба [`java.time.format.DateTimeFormatter.ofPattern(...)`](https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/time/format/DateTimeFormatter.html).

### Канали

Транзакциите могат да се извършват по един от три вида канали: банкомат, онлайн и банков клон, описвани от следния enum:

```java
package bg.sofia.uni.fmi.frauddetector.transaction;

public enum Channel {
ATM, ONLINE, BRANCH
}
```

## Правила за изчисляване на риска

Ще създадем множество правила, чрез които ще оценяваме риска на базата на аномалии в транакциите на дадения акаунт. Всяко правило се характеризира с *праг*, който го задейства (прави приложимо) и относително *тегло*, с което участва в изчислението на риска. Теглото е число в интервала [0.0, 1.0], а сумата от теглата на всички правила трябва да е 1.0.

1. **FrequencyRule**: Оценява риска на базата на *честота на транзакциите*, т.е. броят им в определен времеви прозорец.
- Праг: брой транзакции в рамките на времевия прозорец.

Класът трябва да има публичен конструктор със сигнатура `FrequencyRule(int transactionCountThreshold, TemporalAmount timeWindow, double weight)`.

2. **LocationsRule**: Оценява риска на базата на броя различни *локации на транзакциите*.
- Праг: брой различни локации, от които са осъществени транзакции.

Класът трябва да има публичен конструктор със сигнатура `LocationsRule(int threshold, double weight)`.

3. **SmallTransactionsRule**: Оценява риска на базата на твърде голям брой транзакции на дребна сума.
- Праг: брой транзакции на сума под определен размер.

Класът трябва да има публичен конструктор със сигнатура `SmallTransactionsRule(int countThreshold, double amountThreshold, double weight)`.

4. **ZScoreRule**: Оценява риска въз основа на т.нар. *Z-score* на сумата на транзакциите, което е метрика за това, колко се различава сумата на транзакцията от средната стойност за дадения акаунт.
- Праг: Z-score над определено число.

Класът трябва да има публичен конструктор със сигнатура `ZScoreRule(double zScoreThreshold, double weight)`.

:point_right: Подсказка: За да разберем z-score метриката, трябва да се запознаем с някои понятия от статистиката:

- *Нормално разпределение* (*Normal distribution*) на данните в един dataset означава, че повечето данни са концентрирани около средната стойност и са симетрични спрямо нея.

- *Дисперсия* (*Variance*) е мярка за "разпръснатостта" на стойностите в даден набор от данни, т.е. колко далеч от средната стойност стигат.

Дисперсията се изчислява по следната формула:

![Variance formula](../lecture/images/08.3-variance-formula.png)

където *μ* е средната стойност, т.е.

![Average value formula](../lecture/images/08.4-average-value-formula.png)

- *Стандартно отклонение* (*Standard deviation*) е една от основните мерки за оценка на разсейването или разпространението на стойностите в един набор от данни. То показва, колко далеч са отделните стойности от средната стойност на тези данни. Стандартното отклонение се пресмята като квадратния корен на дисперсията:

![Standard deviation formula](../lecture/images/08.5-standard-deviation-formula.png)

- *Z-score* е метрика за това, колко далеч е дадена стойност от средната стойност, но измерено в брой стандартни отклонения.

Формулата за изчисляване на z-score е следната:

![Z-score formula](../lecture/images/08.6-z-score-formula.png)

където:
- *x* е стойността на данната,
- *μ* е средната стойност,
- *σ* е стандартното отклонение.

Ако ви е любопитно, може да научите повече за [нормалното разпределение](https://www.mathsisfun.com/data/standard-normal-distribution.html), [стандартното отклонение и дисперсията](https://www.mathsisfun.com/data/standard-deviation.html) и [z-score-а](https://www.mathsisfun.com/definitions/z-score.html) - ще ви е полезно за курса по статистика.

Всички правила се намират в пакета `bg.sofia.uni.fmi.frauddetector.rule` и имплементират интерфейса `Rule`:

```java
package bg.sofia.uni.fmi.frauddetector.rule;

import bg.sofia.uni.fmi.frauddetector.transaction.Transaction;

import java.util.List;

/**
* Represents a generic rule that can be applied to a list of transactions.
* This interface defines the structure for determining the applicability of
* a rule and its associated weight. If you want to dig deeper,
* you can check <a href="https://medium.com/swlh/rules-pattern-1c59854547b">this article</a>
* on the Rules pattern.
*/
public interface Rule {

/**
* Determines whether the rule is applicable based on the given list of transactions.
*
* @param transactions the list of objects to evaluate.
* These transactions are used to determine if the rule
* conditions are satisfied.
* @return true, if the rule is applicable based on the transactions.
*/
boolean applicable(List<Transaction> transactions);

/**
* Retrieves the weight of the rule.
* The weight represents the importance or priority of the rule
* and is a double-precision floating-point number in the interval [0, 1].
*
* @return the weight of the rule.
*/
double weight();

}
```

### Примерна употреба

Ето един прост пример, как може да се използва Fraud Detector-a:

```java
import bg.sofia.uni.fmi.frauddetector.analyzer.TransactionAnalyzer;
import bg.sofia.uni.fmi.frauddetector.analyzer.TransactionAnalyzerImpl;
import bg.sofia.uni.fmi.frauddetector.rule.FrequencyRule;
import bg.sofia.uni.fmi.frauddetector.rule.LocationsRule;
import bg.sofia.uni.fmi.frauddetector.rule.Rule;
import bg.sofia.uni.fmi.frauddetector.rule.SmallTransactionsRule;
import bg.sofia.uni.fmi.frauddetector.rule.ZScoreRule;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.Reader;
import java.time.Period;
import java.util.List;

public class Main {

public static void main(String... args) throws FileNotFoundException {
String filePath = "dataset.csv";

Reader reader = new FileReader(filePath);
List<Rule> rules = List.of(
new ZScoreRule(1.5, 0.3),
new LocationsRule(3, 0.4),
new FrequencyRule(4, Period.ofWeeks(4), 0.25),
new SmallTransactionsRule(1, 10.20, 0.05)
);

TransactionAnalyzer analyzer = new TransactionAnalyzerImpl(reader, rules);

System.out.println(analyzer.allAccountIDs());
System.out.println(analyzer.allTransactionsByUser(analyzer.allTransactions().getFirst().accountID()));
System.out.println(analyzer.accountsRisk());
}

}
```

### Тестване

Най-добре първо тествайте реализацията си локално с пример като горния. После създайте и unit тестове.

## Пакети

Спазвайте имената на пакетите на всички по-горе описани класове, тъй като в противен случай решението ви няма да може да бъде тествано от грейдъра.

```
src
└── bg.sofia.uni.fmi.frauddetector
├── analyzer
│ ├── TransactionAnalyzer.java
│ ├── TransactionAnalyzerImpl.java
│ └── (...)
├── rule
│ ├── FrequencyRule.java
│ ├── LocationsRule.java
│ ├── Rule.java
│ ├── SmallTransactionsRule.java
│ ├── ZScoreRule.java
│ └── (...)
├── transaction
│ ├── Channel.java
│ ├── Transaction.java
│ └── (...)
└── (...)
test
└── bg.sofia.uni.fmi.frauddetector
└── (...)
```

## Забележки

- В грейдъра качете директориите src и test като ги селектирате и двете. Друг вариант е да ги сложите в общ .zip архив и да качите него
- Не качвайте jar-ките на JUnit и Mockito библиотеките. На грейдъра ги има, няма смисъл решението ви да набъбва излишно.

Успех!
Loading

0 comments on commit ba181ea

Please sign in to comment.