diff --git a/backend/package-lock.json b/backend/package-lock.json index 30e373bf2c516580af16ea2e337b809996b3f40e..07cd3119f6853d9f3c86da6475a060de5a3d8396 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -18,6 +18,7 @@ "@nestjs/serve-static": "^4.0.2", "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.2", + "axios": "^1.7.5", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", @@ -3373,6 +3374,16 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/axios": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", + "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -5321,6 +5332,25 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -5390,7 +5420,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -8288,6 +8317,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/backend/src/route/dto/recommend-route.dto.ts b/backend/src/route/dto/recommend-route.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..66e99e4038e4c13248b273103534d304b576cc4a --- /dev/null +++ b/backend/src/route/dto/recommend-route.dto.ts @@ -0,0 +1,7 @@ +export interface RecommendPlace { + idPlace: number; + openAt: number; + closeAt: number; + categories: number[]; + grade: number; +} diff --git a/backend/src/route/route.controller.ts b/backend/src/route/route.controller.ts index fc7d8394cb8e0ce28390c2a2b8b68dadc739c98e..0d8c512f30627223e3dcb27ba05450f46847eff7 100644 --- a/backend/src/route/route.controller.ts +++ b/backend/src/route/route.controller.ts @@ -4,8 +4,8 @@ import { CreateRouteDto } from './dto/create-route.dto'; import { UpdateRouteDto } from './dto/update-route.dto'; import { ApiConsumes, ApiTags } from '@nestjs/swagger'; -@Controller('Route') -@ApiTags('testing') +@Controller('route') +@ApiTags('routes') export class RouteController { constructor(private readonly routeService: RouteService) {} @@ -35,4 +35,9 @@ export class RouteController { remove(@Param('id') id: string) { return this.routeService.remove(+id); } + + @Get('recommend') + recommendRoute() { + return this.routeService.recommend(); + } } diff --git a/backend/src/route/route.service.ts b/backend/src/route/route.service.ts index 51c2f121e1a8b8a644993700fc5e05722deeac4f..c5da153b7586f6ec4d96fec4e4cb87ebe02f8b70 100644 --- a/backend/src/route/route.service.ts +++ b/backend/src/route/route.service.ts @@ -8,6 +8,7 @@ import { User } from 'src/user/entities/user.entity'; import { Town } from 'src/town/entities/town.entity'; import { TravelPlaceService } from 'src/travel-place/travel-place.service'; import { TravelPlace } from 'src/travel-place/entities/travel-place.entity'; +import { RecommendationsSystem } from './utils/recommendations'; @Injectable() export class RouteService { @@ -52,4 +53,11 @@ export class RouteService { remove(id: number) { return `This action removes a #${id} route`; } + + recommend() { + // Obtener los visitados y los candidatos + const system = new RecommendationsSystem(); + // system.recommend(visited, candidates); + } + } diff --git a/backend/src/route/utils/math.ts b/backend/src/route/utils/math.ts new file mode 100644 index 0000000000000000000000000000000000000000..f6b24547db2c81d17e098ade2431cf2995fa5b90 --- /dev/null +++ b/backend/src/route/utils/math.ts @@ -0,0 +1,7 @@ +export interface DataFrame { + [key: number]: number[]; +} + +export interface Series { + [index: number]: number; +} diff --git a/backend/src/route/utils/recommendations.ts b/backend/src/route/utils/recommendations.ts new file mode 100644 index 0000000000000000000000000000000000000000..5835dcec9b7d708a74bcd26f4963dfeb1bd6fec5 --- /dev/null +++ b/backend/src/route/utils/recommendations.ts @@ -0,0 +1,70 @@ +import { RecommendPlace } from '../dto/recommend-route.dto'; +import { DataFrame, Series } from './math'; +import { customSort } from './sort'; + +export class RecommendationsSystem { + getCategories(places: RecommendPlace[]): number[] { + const categories = new Set(); + for (const place of places) { + for (const category of place.categories) { + categories.add(category); + } + } + return Array.from(categories); + } + + oneHotEncode(categories: number[], placesToEncode: RecommendPlace[]): DataFrame { + const data: DataFrame = {}; + for (const category of categories) { + data[category] = []; + for (const place of placesToEncode) { + data[category].push(place.categories.includes(category) ? 1 : 0); + } + } + return data; + } + + rankVisited(visited: RecommendPlace[]): Series { + const visitedCategories = this.getCategories(visited); + const visitedEncoded = this.oneHotEncode(visitedCategories, visited); + const grades = visited.map((place) => place.grade); + + const dataframe: DataFrame = visitedEncoded; + + const result: Series = {}; + for (const category in dataframe) { + result[category] = dataframe[category].reduce((sum, value, index) => sum + value * grades[index], 0); + } + + const sumResult = Object.values(result).reduce((a, b) => a + b, 0); + for (const category in result) { + result[category] /= sumResult; + } + + return result; + } + + rankCandidates(candidates: RecommendPlace[], visited: Series): [number, number][] { + const visitedCategories = Object.keys(visited).map(Number); + const candidatesEncoded = this.oneHotEncode(visitedCategories, candidates); + + const dataframe: DataFrame = candidatesEncoded; + + const result: number[] = candidates.map((_, i) => { + return visitedCategories.reduce((sum, category) => { + return sum + dataframe[category][i] * visited[category]; + }, 0); + }); + + let ranked: [number, number][] = candidates.map((place, i) => [place.idPlace, result[i]]); + ranked.sort(customSort); + + return ranked; + } + + recommend(visited: RecommendPlace[], candidates: RecommendPlace[]) { + const visitedRanking = this.rankVisited(visited); + const candidatesRanked = this.rankCandidates(candidates, visitedRanking); + return candidatesRanked; + } +} diff --git a/backend/src/route/utils/sort.ts b/backend/src/route/utils/sort.ts new file mode 100644 index 0000000000000000000000000000000000000000..4da8984a3f7d64ac61fdc37aebe6ec1178b72ff3 --- /dev/null +++ b/backend/src/route/utils/sort.ts @@ -0,0 +1,3 @@ +export function customSort(a: [number, number], b: [number, number]): number { + return b[1] - a[1]; +}