From 16d6d32582676bd7fef522b928bf01df6bce0801 Mon Sep 17 00:00:00 2001 From: jpjon Date: Wed, 30 Aug 2023 10:20:25 -0700 Subject: [PATCH 01/14] Init draft --- .../recommendation/recommender.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py new file mode 100644 index 0000000..017ff53 --- /dev/null +++ b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py @@ -0,0 +1,77 @@ +import json +import pandas as pd +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.metrics.pairwise import cosine_similarity +from recommender_helper import ( + content_movie_recommender, + get_popularity_rmse, + get_vote_avg_rmse, + get_vote_count_rmse, +) + + +def connect_duckdb(): + """ + Function that automatically connects + to duckdb as a GET call upon launch + of FastAPI + + Returns a connection + """ + # Will have to adjust this based on + # how we set up duckdb instance + pass + + +def create_combined(df, weight=2): + df["combined"] = df["overview"] + " " + (df["genre_names"] + ", ") * weight + return df + + +def get_recommendation(movie: str, num_rec: int = 10, stop_words="english"): + # conn = connect_duckdb() + + # use sql/jupysql to query data + # convert data to pandas pd called df + + # Create column with overview and genres + df = create_combined(1) + + # Vectorize "combined" + tfidf = TfidfVectorizer(stop_words=stop_words) + tfidf_matrix = tfidf.fit_transform(df["combined"]) + + # Compute similarity + similarity = cosine_similarity(tfidf_matrix) + + similarity_df = pd.DataFrame( + similarity, index=df.title.values, columns=df.title.values + ) + + movie_list = similarity_df.columns.values + + # Get movie recommendations + recommendations = content_movie_recommender( + movie, similarity_df, movie_list, num_rec + ) + + # Compute metrics + popularity_rmse = get_popularity_rmse(df, movie, recommendations) + + vote_avg_rmse = get_vote_avg_rmse(df, movie, recommendations) + + vote_count_rmse = get_vote_count_rmse(df, movie, recommendations) + + result = { + "movie": movie, + "recommendations": recommendations, + "metrics": { + "popularity": popularity_rmse, + "vote_avg": vote_avg_rmse, + "vote_count": vote_count_rmse, + }, + } + + result_json = json.dumps(result) + + return result_json From 933b2c1c133bd2bef3b45116f731e28eb3e85269 Mon Sep 17 00:00:00 2001 From: jpjon Date: Wed, 30 Aug 2023 15:13:23 -0700 Subject: [PATCH 02/14] current WIP --- .../movie_rec_system/recommendation/app.py | 23 ++++++++ .../recommendation/recommender.py | 54 +++++++++++++++---- 2 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 mini-projects/movie-rec-system/movie_rec_system/recommendation/app.py diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/app.py b/mini-projects/movie-rec-system/movie_rec_system/recommendation/app.py new file mode 100644 index 0000000..1db72a6 --- /dev/null +++ b/mini-projects/movie-rec-system/movie_rec_system/recommendation/app.py @@ -0,0 +1,23 @@ +from fastapi import FastAPI + +from recommender import get_recommendation + +app = FastAPI() + + +@app.get("/recommendations/") +def get_movie_recommendations( + movie: str, num_rec: int = 10, stop_words: str = "english" +): + """ + Get movie recommendations for a given movie. + + Parameters: + - movie: The name of the movie for which you want recommendations. + - num_rec: The number of movie recommendations you want. Default is 10. + - stop_words: The language for stop words. Default is "english". + + Returns: + JSON containing recommended movies and metrics. + """ + return get_recommendation(movie, num_rec, stop_words) diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py index 017ff53..2ab6c1f 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py +++ b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py @@ -1,5 +1,6 @@ import json import pandas as pd +import duckdb from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity from recommender_helper import ( @@ -10,7 +11,7 @@ ) -def connect_duckdb(): +def get_data(): """ Function that automatically connects to duckdb as a GET call upon launch @@ -18,24 +19,57 @@ def connect_duckdb(): Returns a connection """ - # Will have to adjust this based on - # how we set up duckdb instance - pass + con = duckdb.connect("../../movies_data.duckdb") + query = "SELECT * FROM movie_genre_data" + df = con.execute(query).fetchdf() + con.close() + return df + +def create_combined(df: pd.DataFrame, weight=2): + """ + Generates a "combined" column by combining the + "overview" and "genre_names" columns. + + The "genre_names" column will be multiplied by the + provided weight, essentially repeating the genre names + the specified number of times. + + Parameters + ---------- + df : pd.DataFrame + The input DataFrame which must contain + both "overview" and "genre_names" columns. + + weight : int, default=2 + The number of times "genre_names" should be + repeated in the "combined" column. + + Returns + ------- + pd.DataFrame + The modified DataFrame with an additional "combined" column. + + Examples + -------- + >>> df = pd.DataFrame({ + ... 'overview': ['A story about...'], + ... 'genre_names': ['Action'] + ... }) + >>> create_combined(df) + overview genre_names combined + 0 A story about... Action A story about... Action, Action, -def create_combined(df, weight=2): + """ df["combined"] = df["overview"] + " " + (df["genre_names"] + ", ") * weight return df def get_recommendation(movie: str, num_rec: int = 10, stop_words="english"): - # conn = connect_duckdb() - - # use sql/jupysql to query data - # convert data to pandas pd called df + df = get_data() # Create column with overview and genres - df = create_combined(1) + df = create_combined(df) # Vectorize "combined" tfidf = TfidfVectorizer(stop_words=stop_words) From e3dc9499ec5c62c615db11a9b43012522f7e1d26 Mon Sep 17 00:00:00 2001 From: jpjon Date: Thu, 31 Aug 2023 12:34:47 -0700 Subject: [PATCH 03/14] FastAPi now working. Also added docstring to script to make it more understandable --- .../recommendation/recommender.ipynb | 1001 +---------------- .../recommendation/recommender.py | 44 +- .../recommendation/recommender_helper.py | 49 +- 3 files changed, 109 insertions(+), 985 deletions(-) diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.ipynb b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.ipynb index 2874407..7cd909e 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.ipynb +++ b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.ipynb @@ -13,7 +13,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -37,22 +37,9 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Found pyproject.toml from 'C:\\Users\\jpjon\\Documents\\Ploomber\\sql\\mini-projects\\movie-rec-system'" - ], - "text/plain": [ - "Found pyproject.toml from 'C:\\Users\\jpjon\\Documents\\Ploomber\\sql\\mini-projects\\movie-rec-system'" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%reload_ext sql\n", "%sql duckdb:///../../movies_data.duckdb" @@ -60,252 +47,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "Running query in 'duckdb:///../../movies_data.duckdb'" - ], - "text/plain": [ - "Running query in 'duckdb:///../../movies_data.duckdb'" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
genre_namesidoriginal_languageoverviewpopularityrelease_datetitlevote_averagevote_count
0Thriller, Action724209enAn intelligence operative for a shadowy global...2813.2992023-08-09Heart of Stone6.9700
1Animation, Action, Adventure569094enAfter reuniting with Gwen Stacy, Brooklyn’s fu...1738.3082023-05-31Spider-Man: Across the Spider-Verse8.53696
2Action, Adventure, Science Fiction298618enWhen his attempt to save his family inadverten...1559.1712023-06-13The Flash7.02443
3Comedy, Adventure, Fantasy346698enBarbie and Ken are having the time of their li...1556.6612023-07-19Barbie7.43309
4Animation, Science Fiction, Action, Adventure1121575enTravel across the galaxy with John Sheridan as...1519.6102023-08-15Babylon 5: The Road Home7.622
..............................
980Action, Comedy, Science Fiction257344enVideo game experts are recruited by the milita...73.2422015-07-16Pixels5.77013
981Action, Crime, Thriller273481enAn idealistic FBI agent is enlisted by a gover...70.2842015-09-17Sicario7.47754
982Horror109428enFive young friends find the mysterious and fie...45.7532013-04-05Evil Dead6.54190
983Action, Adventure, Science Fiction141052enFuelled by his restored faith in humanity and ...71.8162017-11-15Justice League6.112200
984Adventure, Fantasy, Action18823enBorn of a god but raised as a man, Perseus is ...52.6912010-03-31Clash of the Titans5.95824
\n", - "

985 rows × 9 columns

\n", - "
" - ], - "text/plain": [ - " genre_names id original_language \\\n", - "0 Thriller, Action 724209 en \n", - "1 Animation, Action, Adventure 569094 en \n", - "2 Action, Adventure, Science Fiction 298618 en \n", - "3 Comedy, Adventure, Fantasy 346698 en \n", - "4 Animation, Science Fiction, Action, Adventure 1121575 en \n", - ".. ... ... ... \n", - "980 Action, Comedy, Science Fiction 257344 en \n", - "981 Action, Crime, Thriller 273481 en \n", - "982 Horror 109428 en \n", - "983 Action, Adventure, Science Fiction 141052 en \n", - "984 Adventure, Fantasy, Action 18823 en \n", - "\n", - " overview popularity \\\n", - "0 An intelligence operative for a shadowy global... 2813.299 \n", - "1 After reuniting with Gwen Stacy, Brooklyn’s fu... 1738.308 \n", - "2 When his attempt to save his family inadverten... 1559.171 \n", - "3 Barbie and Ken are having the time of their li... 1556.661 \n", - "4 Travel across the galaxy with John Sheridan as... 1519.610 \n", - ".. ... ... \n", - "980 Video game experts are recruited by the milita... 73.242 \n", - "981 An idealistic FBI agent is enlisted by a gover... 70.284 \n", - "982 Five young friends find the mysterious and fie... 45.753 \n", - "983 Fuelled by his restored faith in humanity and ... 71.816 \n", - "984 Born of a god but raised as a man, Perseus is ... 52.691 \n", - "\n", - " release_date title vote_average \\\n", - "0 2023-08-09 Heart of Stone 6.9 \n", - "1 2023-05-31 Spider-Man: Across the Spider-Verse 8.5 \n", - "2 2023-06-13 The Flash 7.0 \n", - "3 2023-07-19 Barbie 7.4 \n", - "4 2023-08-15 Babylon 5: The Road Home 7.6 \n", - ".. ... ... ... \n", - "980 2015-07-16 Pixels 5.7 \n", - "981 2015-09-17 Sicario 7.4 \n", - "982 2013-04-05 Evil Dead 6.5 \n", - "983 2017-11-15 Justice League 6.1 \n", - "984 2010-03-31 Clash of the Titans 5.9 \n", - "\n", - " vote_count \n", - "0 700 \n", - "1 3696 \n", - "2 2443 \n", - "3 3309 \n", - "4 22 \n", - ".. ... \n", - "980 7013 \n", - "981 7754 \n", - "982 4190 \n", - "983 12200 \n", - "984 5824 \n", - "\n", - "[985 rows x 9 columns]" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df = %sql select * from movie_genre_data\n", "df = pd.DataFrame(df)\n", @@ -334,7 +78,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -354,286 +98,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Heart of StoneSpider-Man: Across the Spider-VerseThe FlashBarbieBabylon 5: The Road HomeNo Hard FeelingsMeg 2: The TrenchCobwebFast XInsidious: The Red Door...Just Go with ItNational Lampoon's VacationThe Twilight Saga: New MoonDawn of the Planet of the ApesGhostbustersPixelsSicarioEvil DeadJustice LeagueClash of the Titans
Heart of Stone1.0000000.0191180.0224500.0124760.0066210.0217040.0187390.0320520.0351220.020683...0.0162030.0166910.0163480.0049650.0305420.0027970.0150280.0461100.0167350.035632
Spider-Man: Across the Spider-Verse0.0191181.0000000.1004350.0499980.1003440.0293350.0330160.0252040.0795030.032446...0.0211710.0382980.0373710.0305050.0296160.0170290.0429880.0320740.0627350.057223
The Flash0.0224500.1004351.0000000.0864770.0900020.0565230.0246590.0912110.0733120.059254...0.1218670.0873340.0741850.0757160.0201780.0272830.0585680.0425690.1537520.095988
Barbie0.0124760.0499980.0864771.0000000.0430660.0394750.0452930.0145500.0813650.076820...0.0681600.0703230.0302410.0401760.0261610.0234960.0390550.0434080.0617840.031715
Babylon 5: The Road Home0.0066210.1003440.0900020.0430661.0000000.0325740.0186920.0338060.0346730.033512...0.0226760.0552410.0416480.0396120.0148440.0178550.0301330.0678700.0347410.042810
\n", - "

5 rows × 985 columns

\n", - "
" - ], - "text/plain": [ - " Heart of Stone \\\n", - "Heart of Stone 1.000000 \n", - "Spider-Man: Across the Spider-Verse 0.019118 \n", - "The Flash 0.022450 \n", - "Barbie 0.012476 \n", - "Babylon 5: The Road Home 0.006621 \n", - "\n", - " Spider-Man: Across the Spider-Verse \\\n", - "Heart of Stone 0.019118 \n", - "Spider-Man: Across the Spider-Verse 1.000000 \n", - "The Flash 0.100435 \n", - "Barbie 0.049998 \n", - "Babylon 5: The Road Home 0.100344 \n", - "\n", - " The Flash Barbie \\\n", - "Heart of Stone 0.022450 0.012476 \n", - "Spider-Man: Across the Spider-Verse 0.100435 0.049998 \n", - "The Flash 1.000000 0.086477 \n", - "Barbie 0.086477 1.000000 \n", - "Babylon 5: The Road Home 0.090002 0.043066 \n", - "\n", - " Babylon 5: The Road Home \\\n", - "Heart of Stone 0.006621 \n", - "Spider-Man: Across the Spider-Verse 0.100344 \n", - "The Flash 0.090002 \n", - "Barbie 0.043066 \n", - "Babylon 5: The Road Home 1.000000 \n", - "\n", - " No Hard Feelings Meg 2: The Trench \\\n", - "Heart of Stone 0.021704 0.018739 \n", - "Spider-Man: Across the Spider-Verse 0.029335 0.033016 \n", - "The Flash 0.056523 0.024659 \n", - "Barbie 0.039475 0.045293 \n", - "Babylon 5: The Road Home 0.032574 0.018692 \n", - "\n", - " Cobweb Fast X \\\n", - "Heart of Stone 0.032052 0.035122 \n", - "Spider-Man: Across the Spider-Verse 0.025204 0.079503 \n", - "The Flash 0.091211 0.073312 \n", - "Barbie 0.014550 0.081365 \n", - "Babylon 5: The Road Home 0.033806 0.034673 \n", - "\n", - " Insidious: The Red Door ... \\\n", - "Heart of Stone 0.020683 ... \n", - "Spider-Man: Across the Spider-Verse 0.032446 ... \n", - "The Flash 0.059254 ... \n", - "Barbie 0.076820 ... \n", - "Babylon 5: The Road Home 0.033512 ... \n", - "\n", - " Just Go with It \\\n", - "Heart of Stone 0.016203 \n", - "Spider-Man: Across the Spider-Verse 0.021171 \n", - "The Flash 0.121867 \n", - "Barbie 0.068160 \n", - "Babylon 5: The Road Home 0.022676 \n", - "\n", - " National Lampoon's Vacation \\\n", - "Heart of Stone 0.016691 \n", - "Spider-Man: Across the Spider-Verse 0.038298 \n", - "The Flash 0.087334 \n", - "Barbie 0.070323 \n", - "Babylon 5: The Road Home 0.055241 \n", - "\n", - " The Twilight Saga: New Moon \\\n", - "Heart of Stone 0.016348 \n", - "Spider-Man: Across the Spider-Verse 0.037371 \n", - "The Flash 0.074185 \n", - "Barbie 0.030241 \n", - "Babylon 5: The Road Home 0.041648 \n", - "\n", - " Dawn of the Planet of the Apes \\\n", - "Heart of Stone 0.004965 \n", - "Spider-Man: Across the Spider-Verse 0.030505 \n", - "The Flash 0.075716 \n", - "Barbie 0.040176 \n", - "Babylon 5: The Road Home 0.039612 \n", - "\n", - " Ghostbusters Pixels Sicario \\\n", - "Heart of Stone 0.030542 0.002797 0.015028 \n", - "Spider-Man: Across the Spider-Verse 0.029616 0.017029 0.042988 \n", - "The Flash 0.020178 0.027283 0.058568 \n", - "Barbie 0.026161 0.023496 0.039055 \n", - "Babylon 5: The Road Home 0.014844 0.017855 0.030133 \n", - "\n", - " Evil Dead Justice League \\\n", - "Heart of Stone 0.046110 0.016735 \n", - "Spider-Man: Across the Spider-Verse 0.032074 0.062735 \n", - "The Flash 0.042569 0.153752 \n", - "Barbie 0.043408 0.061784 \n", - "Babylon 5: The Road Home 0.067870 0.034741 \n", - "\n", - " Clash of the Titans \n", - "Heart of Stone 0.035632 \n", - "Spider-Man: Across the Spider-Verse 0.057223 \n", - "The Flash 0.095988 \n", - "Barbie 0.031715 \n", - "Babylon 5: The Road Home 0.042810 \n", - "\n", - "[5 rows x 985 columns]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Compute cosine similarity between all movie-descriptions\n", "similarity = cosine_similarity(tfidf_matrix)\n", @@ -645,7 +112,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -654,28 +121,17 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "Top Recommended Movies for: Spider-Man: Across the Spider-Verse are:-\n", - " ['Spider-Man: Into the Spider-Verse' 'Spider-Man'\n", - " 'The Amazing Spider-Man 2' 'Spider-Man 3' 'Thor: Ragnarok'\n", - " 'Spider-Man: Homecoming' 'Doctor Strange in the Multiverse of Madness'\n", - " 'The Amazing Spider-Man' 'Sweet Girl' 'Spider-Man: No Way Home']\n" - ] - } - ], + "outputs": [], "source": [ - "sample_movies = [\"Spider-Man: Across the Spider-Verse\"]\n", + "sample_movie = \"Spider-Man: Across the Spider-Verse\"\n", "\n", - "for movie in sample_movies:\n", - " content_movie_recommender(movie, similarity_df, movie_list, 10)" + "recommendations = content_movie_recommender(\n", + " sample_movie, similarity_df, movie_list, 10\n", + ") # noqa E501\n", + "\n", + "recommendations" ] }, { @@ -691,20 +147,9 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'An intelligence operative for a shadowy global peacekeeping agency races to stop a hacker from stealing its most valuable — and dangerous — weapon. Thriller, Action, Thriller, Action, '" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df[\"combined\"] = (\n", " df[\"overview\"] + \" \" + (df[\"genre_names\"] + \", \") * 2\n", @@ -714,7 +159,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -724,286 +169,9 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Heart of StoneSpider-Man: Across the Spider-VerseThe FlashBarbieBabylon 5: The Road HomeNo Hard FeelingsMeg 2: The TrenchCobwebFast XInsidious: The Red Door...Just Go with ItNational Lampoon's VacationThe Twilight Saga: New MoonDawn of the Planet of the ApesGhostbustersPixelsSicarioEvil DeadJustice LeagueClash of the Titans
Heart of Stone1.0000000.0166770.0186750.0000000.0202240.0000000.0231600.0241010.0456810.032084...0.0000000.0000000.0000000.0563740.0435530.0245760.0681520.0000000.0177920.027670
Spider-Man: Across the Spider-Verse0.0166771.0000000.0640580.0304980.0710860.0000000.0281700.0000000.0503960.006986...0.0000000.0208680.0222550.0130800.0313500.0222150.0158120.0000000.0515590.030391
The Flash0.0186750.0640581.0000000.0357020.1064390.0000000.0748180.0000000.0205410.005084...0.0180250.0410400.0441440.0730220.0130850.0793930.0177070.0000000.1848760.037766
Barbie0.0000000.0304980.0357021.0000000.0201880.0379140.0000000.0000000.0000000.000000...0.0605880.0938380.0445790.0000000.0575740.0312390.0000000.0000000.0177600.033077
Babylon 5: The Road Home0.0202240.0710860.1064390.0201881.0000000.0119310.0810250.0000000.0128530.008472...0.0000000.0608020.0269890.0790800.0141710.0956740.0191760.0323570.0775150.020015
\n", - "

5 rows × 985 columns

\n", - "
" - ], - "text/plain": [ - " Heart of Stone \\\n", - "Heart of Stone 1.000000 \n", - "Spider-Man: Across the Spider-Verse 0.016677 \n", - "The Flash 0.018675 \n", - "Barbie 0.000000 \n", - "Babylon 5: The Road Home 0.020224 \n", - "\n", - " Spider-Man: Across the Spider-Verse \\\n", - "Heart of Stone 0.016677 \n", - "Spider-Man: Across the Spider-Verse 1.000000 \n", - "The Flash 0.064058 \n", - "Barbie 0.030498 \n", - "Babylon 5: The Road Home 0.071086 \n", - "\n", - " The Flash Barbie \\\n", - "Heart of Stone 0.018675 0.000000 \n", - "Spider-Man: Across the Spider-Verse 0.064058 0.030498 \n", - "The Flash 1.000000 0.035702 \n", - "Barbie 0.035702 1.000000 \n", - "Babylon 5: The Road Home 0.106439 0.020188 \n", - "\n", - " Babylon 5: The Road Home \\\n", - "Heart of Stone 0.020224 \n", - "Spider-Man: Across the Spider-Verse 0.071086 \n", - "The Flash 0.106439 \n", - "Barbie 0.020188 \n", - "Babylon 5: The Road Home 1.000000 \n", - "\n", - " No Hard Feelings Meg 2: The Trench \\\n", - "Heart of Stone 0.000000 0.023160 \n", - "Spider-Man: Across the Spider-Verse 0.000000 0.028170 \n", - "The Flash 0.000000 0.074818 \n", - "Barbie 0.037914 0.000000 \n", - "Babylon 5: The Road Home 0.011931 0.081025 \n", - "\n", - " Cobweb Fast X \\\n", - "Heart of Stone 0.024101 0.045681 \n", - "Spider-Man: Across the Spider-Verse 0.000000 0.050396 \n", - "The Flash 0.000000 0.020541 \n", - "Barbie 0.000000 0.000000 \n", - "Babylon 5: The Road Home 0.000000 0.012853 \n", - "\n", - " Insidious: The Red Door ... \\\n", - "Heart of Stone 0.032084 ... \n", - "Spider-Man: Across the Spider-Verse 0.006986 ... \n", - "The Flash 0.005084 ... \n", - "Barbie 0.000000 ... \n", - "Babylon 5: The Road Home 0.008472 ... \n", - "\n", - " Just Go with It \\\n", - "Heart of Stone 0.000000 \n", - "Spider-Man: Across the Spider-Verse 0.000000 \n", - "The Flash 0.018025 \n", - "Barbie 0.060588 \n", - "Babylon 5: The Road Home 0.000000 \n", - "\n", - " National Lampoon's Vacation \\\n", - "Heart of Stone 0.000000 \n", - "Spider-Man: Across the Spider-Verse 0.020868 \n", - "The Flash 0.041040 \n", - "Barbie 0.093838 \n", - "Babylon 5: The Road Home 0.060802 \n", - "\n", - " The Twilight Saga: New Moon \\\n", - "Heart of Stone 0.000000 \n", - "Spider-Man: Across the Spider-Verse 0.022255 \n", - "The Flash 0.044144 \n", - "Barbie 0.044579 \n", - "Babylon 5: The Road Home 0.026989 \n", - "\n", - " Dawn of the Planet of the Apes \\\n", - "Heart of Stone 0.056374 \n", - "Spider-Man: Across the Spider-Verse 0.013080 \n", - "The Flash 0.073022 \n", - "Barbie 0.000000 \n", - "Babylon 5: The Road Home 0.079080 \n", - "\n", - " Ghostbusters Pixels Sicario \\\n", - "Heart of Stone 0.043553 0.024576 0.068152 \n", - "Spider-Man: Across the Spider-Verse 0.031350 0.022215 0.015812 \n", - "The Flash 0.013085 0.079393 0.017707 \n", - "Barbie 0.057574 0.031239 0.000000 \n", - "Babylon 5: The Road Home 0.014171 0.095674 0.019176 \n", - "\n", - " Evil Dead Justice League \\\n", - "Heart of Stone 0.000000 0.017792 \n", - "Spider-Man: Across the Spider-Verse 0.000000 0.051559 \n", - "The Flash 0.000000 0.184876 \n", - "Barbie 0.000000 0.017760 \n", - "Babylon 5: The Road Home 0.032357 0.077515 \n", - "\n", - " Clash of the Titans \n", - "Heart of Stone 0.027670 \n", - "Spider-Man: Across the Spider-Verse 0.030391 \n", - "The Flash 0.037766 \n", - "Barbie 0.033077 \n", - "Babylon 5: The Road Home 0.020015 \n", - "\n", - "[5 rows x 985 columns]" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "similarity_combined = cosine_similarity(tfidf_matrix_combined)\n", "\n", @@ -1016,7 +184,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1025,60 +193,16 @@ }, { "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "Top Recommended Movies for: Spider-Man: Across the Spider-Verse are:-\n", - " ['Spider-Man: Into the Spider-Verse' 'Spider-Man' 'Spider-Man 3'\n", - " 'The Amazing Spider-Man 2' 'Spider-Man: Homecoming'\n", - " 'Doctor Strange in the Multiverse of Madness' 'Spider-Man: No Way Home'\n", - " 'Ice Age: Dawn of the Dinosaurs'\n", - " 'Deathstroke: Knights & Dragons - The Movie' 'Big Hero 6']\n" - ] - } - ], - "source": [ - "sample_movies = [\"Spider-Man: Across the Spider-Verse\"]\n", - "\n", - "for movie in sample_movies:\n", - " content_movie_recommender(\n", - " movie, similarity_df_combined, combined_movie_list, 10\n", - " ) # noqa E501" - ] - }, - { - "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "Top Recommended Movies for: Spider-Man: Across the Spider-Verse are:-\n", - " ['Spider-Man: Into the Spider-Verse' 'Spider-Man' 'Spider-Man 3'\n", - " 'The Amazing Spider-Man 2' 'Spider-Man: Homecoming'\n", - " 'Doctor Strange in the Multiverse of Madness' 'Spider-Man: No Way Home'\n", - " 'Ice Age: Dawn of the Dinosaurs'\n", - " 'Deathstroke: Knights & Dragons - The Movie' 'Big Hero 6']\n" - ] - } - ], + "outputs": [], "source": [ - "sample_movies = [\"Spider-Man: Across the Spider-Verse\"]\n", + "sample_movie = \"Spider-Man: Across the Spider-Verse\"\n", + "recommendations = content_movie_recommender(\n", + " sample_movie, similarity_df_combined, combined_movie_list, 10\n", + ") # noqa E501\n", "\n", - "for movie in sample_movies:\n", - " content_movie_recommender(\n", - " movie, similarity_df_combined, combined_movie_list, 10\n", - " ) # noqa E501" + "recommendations" ] }, { @@ -1100,7 +224,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1122,60 +246,22 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "Top Recommended Movies for: Spider-Man: Across the Spider-Verse are:-\n", - " ['Spider-Man: Into the Spider-Verse' 'Spider-Man' 'Spider-Man 3'\n", - " 'The Amazing Spider-Man 2' 'Spider-Man: Homecoming'\n", - " 'Doctor Strange in the Multiverse of Madness' 'Spider-Man: No Way Home'\n", - " 'Ice Age: Dawn of the Dinosaurs'\n", - " 'Deathstroke: Knights & Dragons - The Movie' 'Big Hero 6']\n" - ] - } - ], + "outputs": [], "source": [ "sample_movie = \"Spider-Man: Across the Spider-Verse\"\n", "\n", "recommendations = content_movie_recommender(\n", " sample_movie, similarity_df_combined, combined_movie_list, 10\n", - ") # noqa E501" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array(['Spider-Man: Into the Spider-Verse', 'Spider-Man', 'Spider-Man 3',\n", - " 'The Amazing Spider-Man 2', 'Spider-Man: Homecoming',\n", - " 'Doctor Strange in the Multiverse of Madness',\n", - " 'Spider-Man: No Way Home', 'Ice Age: Dawn of the Dinosaurs',\n", - " 'Deathstroke: Knights & Dragons - The Movie', 'Big Hero 6'],\n", - " dtype=object)" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ + ") # noqa E501\n", + "\n", "recommendations" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1188,20 +274,9 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Root Mean Square Error (RMSE) for:\n", - "Popularity: 1620.75\n", - "Vote Average: 1.37\n", - "Vote Count: 10494.70\n" - ] - } - ], + "outputs": [], "source": [ "print(\n", " f\"Root Mean Square Error (RMSE) for:\\n\"\n", diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py index 2ab6c1f..229fadb 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py +++ b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py @@ -11,7 +11,7 @@ ) -def get_data(): +def get_data() -> pd.DataFrame: """ Function that automatically connects to duckdb as a GET call upon launch @@ -26,7 +26,7 @@ def get_data(): return df -def create_combined(df: pd.DataFrame, weight=2): +def create_combined(df: pd.DataFrame, weight=2) -> pd.DataFrame: """ Generates a "combined" column by combining the "overview" and "genre_names" columns. @@ -66,6 +66,46 @@ def create_combined(df: pd.DataFrame, weight=2): def get_recommendation(movie: str, num_rec: int = 10, stop_words="english"): + """ + Generate movie recommendations based on content similarity and computes associated metrics. + + This function retrieves movie data, calculates cosine similarity between movies using + TF-IDF vectorization of their combined overview and genre, and returns a list of recommended + movies along with certain metrics (popularity, vote average, and vote count RMSE). + + Parameters + ---------- + movie : str + The title of the movie for which recommendations are to be generated. + + num_rec : int, optional + The number of movie recommendations to generate. Default is 10. + + stop_words : str, optional + The language of stop words to be used when vectorizing the "combined" column. + Default is "english". + + Returns + ------- + str + A JSON-formatted string containing the original movie, a list of recommendations, + and associated metrics (popularity, vote average, and vote count RMSE). + + Examples + -------- + >>> result = get_recommendation("Inception", num_rec=5) + >>> print(json.loads(result)) + { + "movie": "Inception", + "recommendations": [...], + "metrics": { + "popularity": ..., + "vote_avg": ..., + "vote_count": ... + } + } + + """ df = get_data() # Create column with overview and genres diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender_helper.py b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender_helper.py index ba67d67..e32f83f 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender_helper.py +++ b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender_helper.py @@ -1,12 +1,13 @@ import numpy as np +import pandas as pd def content_movie_recommender( - input_movie, - similarity_database, - movie_database_list, + input_movie: str, + similarity_database: pd.DataFrame, + movie_database_list: list, top_n=10, -): +) -> list: """ Function that uses a similarity matrix to find similar movies @@ -36,17 +37,15 @@ def content_movie_recommender( # get recommended movie names recommended_movies = movie_list[sorted_movie_ids[1 : top_n + 1]] # noqa E203 - print( - "\n\nTop Recommended Movies for:", - input_movie, - "are:-\n", - recommended_movies, - ) - return recommended_movies + return list(recommended_movies) -def get_popularity_rmse(df, sample_movie, recommendations): - sample_movie_popularity = df[df["title"] == sample_movie].popularity[1] +def get_popularity_rmse( + df: pd.DataFrame, sample_movie: str, recommendations: list +) -> float: + sample_movie_popularity = df[df["title"] == sample_movie].popularity.iloc[ + 0 + ] # noqa E501 recommendations_popularity = df[ df["title"].isin(recommendations) ].popularity.values # noqa E501 @@ -54,11 +53,17 @@ def get_popularity_rmse(df, sample_movie, recommendations): squared_diffs = (sample_movie_popularity - recommendations_popularity) ** 2 rmse = np.sqrt(squared_diffs.mean()) - return rmse + return round(float(rmse), 3) -def get_vote_avg_rmse(df, sample_movie, recommendations): - sample_movie_vote_average = df[df["title"] == sample_movie].vote_average[1] +def get_vote_avg_rmse( + df: pd.DataFrame, sample_movie: str, recommendations: list +) -> float: + sample_movie_vote_average = df[ + df["title"] == sample_movie + ].vote_average.iloc[ # noqa E501 + 0 + ] recommendations_vote_average = df[ df["title"].isin(recommendations) ].vote_average.values @@ -68,11 +73,15 @@ def get_vote_avg_rmse(df, sample_movie, recommendations): ) ** 2 # noqa E501 rmse = np.sqrt(squared_diffs.mean()) - return rmse + return round(float(rmse), 3) -def get_vote_count_rmse(df, sample_movie, recommendations): - sample_movie_popularity = df[df["title"] == sample_movie].vote_count[1] +def get_vote_count_rmse( + df: pd.DataFrame, sample_movie: str, recommendations: list +) -> float: + sample_movie_popularity = df[df["title"] == sample_movie].vote_count.iloc[ + 0 + ] # noqa E501 recommendations_popularity = df[ df["title"].isin(recommendations) ].vote_count.values # noqa E501 @@ -80,4 +89,4 @@ def get_vote_count_rmse(df, sample_movie, recommendations): squared_diffs = (recommendations_popularity - sample_movie_popularity) ** 2 rmse = np.sqrt(squared_diffs.mean()) - return rmse + return round(float(rmse), 3) From a2ef6dd2a728208570240ac7b58966d0b2d4483d Mon Sep 17 00:00:00 2001 From: jpjon Date: Thu, 31 Aug 2023 12:39:03 -0700 Subject: [PATCH 04/14] linting errors --- .../recommendation/recommender.py | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py index 229fadb..00a3783 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py +++ b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py @@ -67,29 +67,38 @@ def create_combined(df: pd.DataFrame, weight=2) -> pd.DataFrame: def get_recommendation(movie: str, num_rec: int = 10, stop_words="english"): """ - Generate movie recommendations based on content similarity and computes associated metrics. + Generate movie recommendations based on + content similarity and computes associated metrics. - This function retrieves movie data, calculates cosine similarity between movies using - TF-IDF vectorization of their combined overview and genre, and returns a list of recommended - movies along with certain metrics (popularity, vote average, and vote count RMSE). + This function retrieves movie data, + calculates cosine similarity between movies using + TF-IDF vectorization of their combined overview + and genre, and returns a list of recommended + movies along with certain metrics + (popularity, vote average, and vote count RMSE). Parameters ---------- movie : str - The title of the movie for which recommendations are to be generated. - + The title of the movie for which + recommendations are to be generated. + num_rec : int, optional - The number of movie recommendations to generate. Default is 10. - + The number of movie recommendations + to generate. Default is 10. + stop_words : str, optional - The language of stop words to be used when vectorizing the "combined" column. + The language of stop words to be + used when vectorizing the "combined" column. Default is "english". Returns ------- str - A JSON-formatted string containing the original movie, a list of recommendations, - and associated metrics (popularity, vote average, and vote count RMSE). + A JSON-formatted string containing + the original movie, a list of recommendations, + and associated metrics + (popularity, vote average, and vote count RMSE). Examples -------- From 0aa1447fbb7367eb906e43a86dd016f598bdd45f Mon Sep 17 00:00:00 2001 From: Laura Gutierrez Funderburk Date: Thu, 31 Aug 2023 20:30:35 -0700 Subject: [PATCH 05/14] upgrade pyproject.toml file --- mini-projects/movie-rec-system/poetry.lock | 316 ++++++++---------- mini-projects/movie-rec-system/pyproject.toml | 4 +- 2 files changed, 138 insertions(+), 182 deletions(-) diff --git a/mini-projects/movie-rec-system/poetry.lock b/mini-projects/movie-rec-system/poetry.lock index db2977d..23122bb 100644 --- a/mini-projects/movie-rec-system/poetry.lock +++ b/mini-projects/movie-rec-system/poetry.lock @@ -25,6 +25,27 @@ files = [ [package.dependencies] textwrap3 = ">=0.9.2" +[[package]] +name = "anyio" +version = "4.0.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"}, + {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.22)"] + [[package]] name = "appnope" version = "0.1.3" @@ -582,6 +603,20 @@ files = [ {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"}, ] +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "executing" version = "1.2.0" @@ -596,6 +631,25 @@ files = [ [package.extras] tests = ["asttokens", "littleutils", "pytest", "rich"] +[[package]] +name = "fastapi" +version = "0.103.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.7" +files = [ + {file = "fastapi-0.103.0-py3-none-any.whl", hash = "sha256:61ab72c6c281205dd0cbaccf503e829a37e0be108d965ac223779a8479243665"}, + {file = "fastapi-0.103.0.tar.gz", hash = "sha256:4166732f5ddf61c33e9fa4664f73780872511e0598d4d5434b1816dc1e6d9421"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.27.0,<0.28.0" +typing-extensions = ">=4.5.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] + [[package]] name = "fastjsonschema" version = "2.18.0" @@ -765,6 +819,17 @@ files = [ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "ipdb" version = "0.13.13" @@ -910,17 +975,6 @@ files = [ {file = "joblib-1.3.2.tar.gz", hash = "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1"}, ] -[[package]] -name = "jsonpointer" -version = "2.4" -description = "Identify specific nodes in a JSON document (RFC 6901)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "joblib-1.3.2-py3-none-any.whl", hash = "sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9"}, - {file = "joblib-1.3.2.tar.gz", hash = "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1"}, -] - [[package]] name = "jsonschema" version = "4.19.0" @@ -958,13 +1012,13 @@ referencing = ">=0.28.0" [[package]] name = "jupysql" -version = "0.9.0" +version = "0.10.0" description = "Better SQL in Jupyter" optional = false python-versions = "*" files = [ - {file = "jupysql-0.9.0-py3-none-any.whl", hash = "sha256:8fcd24d7bbc0379e345d3b578f719f50b8db4ded0288e7d43bb4e9c5b85dd9d7"}, - {file = "jupysql-0.9.0.tar.gz", hash = "sha256:0b754436b8e96fdfcff87e087f42e3f9661241c058495aa189d9d8cea8468f72"}, + {file = "jupysql-0.10.0-py3-none-any.whl", hash = "sha256:beb29b39cb977e5e689442b70ed307a4a66bea1a980c45008d54091f530c3947"}, + {file = "jupysql-0.10.0.tar.gz", hash = "sha256:20aa8bbfa952dfbee24b91692f3f996120d99ecee41b924dca3a8a61b4c51ff0"}, ] [package.dependencies] @@ -979,7 +1033,7 @@ sqlparse = "*" [package.extras] dev = ["black", "duckdb (<0.8.0)", "duckdb-engine", "flake8", "invoke", "ipywidgets", "js2py", "jupysql-plugin", "matplotlib", "pandas", "pkgmt", "polars (==0.17.2)", "psutil", "pyarrow", "pyodbc", "pytest", "twine"] -integration = ["black", "dockerctx", "duckdb (<0.8.0)", "duckdb-engine", "flake8", "invoke", "ipywidgets", "js2py", "jupysql-plugin", "matplotlib", "oracledb", "pandas", "pgspecial (==2.0.1)", "pkgmt", "polars (==0.17.2)", "psutil", "psycopg2-binary", "pyarrow", "pymysql", "pyodbc", "pytest", "python-tds", "snowflake-sqlalchemy", "sqlalchemy-pytds", "twine"] +integration = ["black", "dockerctx", "duckdb (<0.8.0)", "duckdb-engine", "flake8", "invoke", "ipywidgets", "js2py", "jupysql-plugin", "matplotlib", "oracledb", "pandas", "pgspecial (==2.0.1)", "pkgmt", "polars (==0.17.2)", "psutil", "psycopg2-binary", "pyarrow", "pymysql", "pyodbc", "pytest", "python-tds", "redshift-connector", "snowflake-sqlalchemy", "sqlalchemy-pytds", "sqlalchemy-redshift", "twine"] [[package]] name = "jupyter-client" @@ -1868,6 +1922,21 @@ jinja2 = "*" [package.extras] dev = ["flake8", "importlib-resources", "invoke", "ipython", "jedi (==0.17.2)", "nox", "pkgmt", "ploomber", "pytest", "twine", "wheel", "yapf"] +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "posthog" version = "3.0.2" @@ -2171,6 +2240,28 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pytest" +version = "7.4.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -2245,7 +2336,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -2253,15 +2343,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -2278,7 +2361,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -2286,7 +2368,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -2569,142 +2650,6 @@ files = [ {file = "scikit_learn-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:1d54fb9e6038284548072df22fd34777e434153f7ffac72c8596f2d6987110dd"}, ] -[[package]] -name = "rpds-py" -version = "0.10.0" -description = "Python bindings to Rust's persistent data structures (rpds)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "rpds_py-0.10.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:c1e0e9916301e3b3d970814b1439ca59487f0616d30f36a44cead66ee1748c31"}, - {file = "rpds_py-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ce8caa29ebbdcde67e5fd652c811d34bc01f249dbc0d61e5cc4db05ae79a83b"}, - {file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad277f74b1c164f7248afa968700e410651eb858d7c160d109fb451dc45a2f09"}, - {file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8e1c68303ccf7fceb50fbab79064a2636119fd9aca121f28453709283dbca727"}, - {file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:780fcb855be29153901c67fc9c5633d48aebef21b90aa72812fa181d731c6b00"}, - {file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbd7b24d108509a1b9b6679fcc1166a7dd031dbef1f3c2c73788f42e3ebb3beb"}, - {file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0700c2133ba203c4068aaecd6a59bda22e06a5e46255c9da23cbf68c6942215d"}, - {file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:576da63eae7809f375932bfcbca2cf20620a1915bf2fedce4b9cc8491eceefe3"}, - {file = "rpds_py-0.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23750a9b8a329844ba1fe267ca456bb3184984da2880ed17ae641c5af8de3fef"}, - {file = "rpds_py-0.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d08395595c42bcd82c3608762ce734504c6d025eef1c06f42326a6023a584186"}, - {file = "rpds_py-0.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1d7b7b71bcb82d8713c7c2e9c5f061415598af5938666beded20d81fa23e7640"}, - {file = "rpds_py-0.10.0-cp310-none-win32.whl", hash = "sha256:97f5811df21703446b42303475b8b855ee07d6ab6cdf8565eff115540624f25d"}, - {file = "rpds_py-0.10.0-cp310-none-win_amd64.whl", hash = "sha256:cdbed8f21204398f47de39b0a9b180d7e571f02dfb18bf5f1b618e238454b685"}, - {file = "rpds_py-0.10.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:7a3a3d3e4f1e3cd2a67b93a0b6ed0f2499e33f47cc568e3a0023e405abdc0ff1"}, - {file = "rpds_py-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fc72ae476732cdb7b2c1acb5af23b478b8a0d4b6fcf19b90dd150291e0d5b26b"}, - {file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0583f69522732bdd79dca4cd3873e63a29acf4a299769c7541f2ca1e4dd4bc6"}, - {file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8b9a7cd381970e64849070aca7c32d53ab7d96c66db6c2ef7aa23c6e803f514"}, - {file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d292cabd7c8335bdd3237ded442480a249dbcdb4ddfac5218799364a01a0f5c"}, - {file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6903cdca64f1e301af9be424798328c1fe3b4b14aede35f04510989fc72f012"}, - {file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bed57543c99249ab3a4586ddc8786529fbc33309e5e8a1351802a06ca2baf4c2"}, - {file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15932ec5f224b0e35764dc156514533a4fca52dcfda0dfbe462a1a22b37efd59"}, - {file = "rpds_py-0.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb2d59bc196e6d3b1827c7db06c1a898bfa0787c0574af398e65ccf2e97c0fbe"}, - {file = "rpds_py-0.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f99d74ddf9d3b6126b509e81865f89bd1283e3fc1b568b68cd7bd9dfa15583d7"}, - {file = "rpds_py-0.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f70bec8a14a692be6dbe7ce8aab303e88df891cbd4a39af091f90b6702e28055"}, - {file = "rpds_py-0.10.0-cp311-none-win32.whl", hash = "sha256:5f7487be65b9c2c510819e744e375bd41b929a97e5915c4852a82fbb085df62c"}, - {file = "rpds_py-0.10.0-cp311-none-win_amd64.whl", hash = "sha256:748e472345c3a82cfb462d0dff998a7bf43e621eed73374cb19f307e97e08a83"}, - {file = "rpds_py-0.10.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:d4639111e73997567343df6551da9dd90d66aece1b9fc26c786d328439488103"}, - {file = "rpds_py-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4760e1b02173f4155203054f77a5dc0b4078de7645c922b208d28e7eb99f3e2"}, - {file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a6420a36975e0073acaeee44ead260c1f6ea56812cfc6c31ec00c1c48197173"}, - {file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58fc4d66ee349a23dbf08c7e964120dc9027059566e29cf0ce6205d590ed7eca"}, - {file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:063411228b852fb2ed7485cf91f8e7d30893e69b0acb207ec349db04cccc8225"}, - {file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65af12f70355de29e1092f319f85a3467f4005e959ab65129cb697169ce94b86"}, - {file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:298e8b5d8087e0330aac211c85428c8761230ef46a1f2c516d6a2f67fb8803c5"}, - {file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b9bf77008f2c55dabbd099fd3ac87009471d223a1c7ebea36873d39511b780a"}, - {file = "rpds_py-0.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c7853f27195598e550fe089f78f0732c66ee1d1f0eaae8ad081589a5a2f5d4af"}, - {file = "rpds_py-0.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:75dbfd41a61bc1fb0536bf7b1abf272dc115c53d4d77db770cd65d46d4520882"}, - {file = "rpds_py-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b25136212a3d064a8f0b9ebbb6c57094c5229e0de76d15c79b76feff26aeb7b8"}, - {file = "rpds_py-0.10.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:9affee8cb1ec453382c27eb9043378ab32f49cd4bc24a24275f5c39bf186c279"}, - {file = "rpds_py-0.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d55528ef13af4b4e074d067977b1f61408602f53ae4537dccf42ba665c2c7bd"}, - {file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7865df1fb564092bcf46dac61b5def25342faf6352e4bc0e61a286e3fa26a3d"}, - {file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f5cc8c7bc99d2bbcd704cef165ca7d155cd6464c86cbda8339026a42d219397"}, - {file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbae50d352e4717ffc22c566afc2d0da744380e87ed44a144508e3fb9114a3f4"}, - {file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fccbf0cd3411719e4c9426755df90bf3449d9fc5a89f077f4a7f1abd4f70c910"}, - {file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d10c431073dc6ebceed35ab22948a016cc2b5120963c13a41e38bdde4a7212"}, - {file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1b401e8b9aece651512e62c431181e6e83048a651698a727ea0eb0699e9f9b74"}, - {file = "rpds_py-0.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:7618a082c55cf038eede4a918c1001cc8a4411dfe508dc762659bcd48d8f4c6e"}, - {file = "rpds_py-0.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:b3226b246facae14909b465061ddcfa2dfeadb6a64f407f24300d42d69bcb1a1"}, - {file = "rpds_py-0.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a8edd467551c1102dc0f5754ab55cd0703431cd3044edf8c8e7d9208d63fa453"}, - {file = "rpds_py-0.10.0-cp38-none-win32.whl", hash = "sha256:71333c22f7cf5f0480b59a0aef21f652cf9bbaa9679ad261b405b65a57511d1e"}, - {file = "rpds_py-0.10.0-cp38-none-win_amd64.whl", hash = "sha256:a8ab1adf04ae2d6d65835995218fd3f3eb644fe20655ca8ee233e2c7270ff53b"}, - {file = "rpds_py-0.10.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:87c93b25d538c433fb053da6228c6290117ba53ff6a537c133b0f2087948a582"}, - {file = "rpds_py-0.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7996aed3f65667c6dcc8302a69368435a87c2364079a066750a2eac75ea01e"}, - {file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8856aa76839dc234d3469f1e270918ce6bec1d6a601eba928f45d68a15f04fc3"}, - {file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00215f6a9058fbf84f9d47536902558eb61f180a6b2a0fa35338d06ceb9a2e5a"}, - {file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23a059143c1393015c68936370cce11690f7294731904bdae47cc3e16d0b2474"}, - {file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e5c26905aa651cc8c0ddc45e0e5dea2a1296f70bdc96af17aee9d0493280a17"}, - {file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c651847545422c8131660704c58606d841e228ed576c8f1666d98b3d318f89da"}, - {file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:80992eb20755701753e30a6952a96aa58f353d12a65ad3c9d48a8da5ec4690cf"}, - {file = "rpds_py-0.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ffcf18ad3edf1c170e27e88b10282a2c449aa0358659592462448d71b2000cfc"}, - {file = "rpds_py-0.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08e08ccf5b10badb7d0a5c84829b914c6e1e1f3a716fdb2bf294e2bd01562775"}, - {file = "rpds_py-0.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7150b83b3e3ddaac81a8bb6a9b5f93117674a0e7a2b5a5b32ab31fdfea6df27f"}, - {file = "rpds_py-0.10.0-cp39-none-win32.whl", hash = "sha256:3455ecc46ea443b5f7d9c2f946ce4017745e017b0d0f8b99c92564eff97e97f5"}, - {file = "rpds_py-0.10.0-cp39-none-win_amd64.whl", hash = "sha256:afe6b5a04b2ab1aa89bad32ca47bf71358e7302a06fdfdad857389dca8fb5f04"}, - {file = "rpds_py-0.10.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:b1cb078f54af0abd835ca76f93a3152565b73be0f056264da45117d0adf5e99c"}, - {file = "rpds_py-0.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8e7e2b3577e97fa43c2c2b12a16139b2cedbd0770235d5179c0412b4794efd9b"}, - {file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae46a50d235f1631d9ec4670503f7b30405103034830bc13df29fd947207f795"}, - {file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f869e34d2326e417baee430ae998e91412cc8e7fdd83d979277a90a0e79a5b47"}, - {file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d544a614055b131111bed6edfa1cb0fb082a7265761bcb03321f2dd7b5c6c48"}, - {file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9c2f6ca9774c2c24bbf7b23086264e6b5fa178201450535ec0859739e6f78d"}, - {file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2da4a8c6d465fde36cea7d54bf47b5cf089073452f0e47c8632ecb9dec23c07"}, - {file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac00c41dd315d147b129976204839ca9de699d83519ff1272afbe4fb9d362d12"}, - {file = "rpds_py-0.10.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:0155c33af0676fc38e1107679be882077680ad1abb6303956b97259c3177e85e"}, - {file = "rpds_py-0.10.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:db6585b600b2e76e98131e0ac0e5195759082b51687ad0c94505970c90718f4a"}, - {file = "rpds_py-0.10.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:7b6975d3763d0952c111700c0634968419268e6bbc0b55fe71138987fa66f309"}, - {file = "rpds_py-0.10.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:6388e4e95a26717b94a05ced084e19da4d92aca883f392dffcf8e48c8e221a24"}, - {file = "rpds_py-0.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:18f87baa20e02e9277ad8960cd89b63c79c05caf106f4c959a9595c43f2a34a5"}, - {file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f05fc7d832e970047662b3440b190d24ea04f8d3c760e33e7163b67308c878"}, - {file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:291c9ce3929a75b45ce8ddde2aa7694fc8449f2bc8f5bd93adf021efaae2d10b"}, - {file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:861d25ae0985a1dd5297fee35f476b60c6029e2e6e19847d5b4d0a43a390b696"}, - {file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:668d2b45d62c68c7a370ac3dce108ffda482b0a0f50abd8b4c604a813a59e08f"}, - {file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:344b89384c250ba6a4ce1786e04d01500e4dac0f4137ceebcaad12973c0ac0b3"}, - {file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:885e023e73ce09b11b89ab91fc60f35d80878d2c19d6213a32b42ff36543c291"}, - {file = "rpds_py-0.10.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:841128a22e6ac04070a0f84776d07e9c38c4dcce8e28792a95e45fc621605517"}, - {file = "rpds_py-0.10.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:899b5e7e2d5a8bc92aa533c2d4e55e5ebba095c485568a5e4bedbc163421259a"}, - {file = "rpds_py-0.10.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e7947d9a6264c727a556541b1630296bbd5d0a05068d21c38dde8e7a1c703ef0"}, - {file = "rpds_py-0.10.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4992266817169997854f81df7f6db7bdcda1609972d8ffd6919252f09ec3c0f6"}, - {file = "rpds_py-0.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:26d9fd624649a10e4610fab2bc820e215a184d193e47d0be7fe53c1c8f67f370"}, - {file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0028eb0967942d0d2891eae700ae1a27b7fd18604cfcb16a1ef486a790fee99e"}, - {file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9e7e493ded7042712a374471203dd43ae3fff5b81e3de1a0513fa241af9fd41"}, - {file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d68a8e8a3a816629283faf82358d8c93fe5bd974dd2704152394a3de4cec22a"}, - {file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6d5f061f6a2aa55790b9e64a23dfd87b6664ab56e24cd06c78eb43986cb260b"}, - {file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c7c4266c1b61eb429e8aeb7d8ed6a3bfe6c890a1788b18dbec090c35c6b93fa"}, - {file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:80772e3bda6787510d9620bc0c7572be404a922f8ccdfd436bf6c3778119464c"}, - {file = "rpds_py-0.10.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:b98e75b21fc2ba5285aef8efaf34131d16af1c38df36bdca2f50634bea2d3060"}, - {file = "rpds_py-0.10.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:d63787f289944cc4bde518ad2b5e70a4f0d6e2ce76324635359c74c113fd188f"}, - {file = "rpds_py-0.10.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:872f3dcaa8bf2245944861d7311179d2c0c9b2aaa7d3b464d99a7c2e401f01fa"}, - {file = "rpds_py-0.10.0.tar.gz", hash = "sha256:e36d7369363d2707d5f68950a64c4e025991eb0177db01ccb6aa6facae48b69f"}, -] - -[[package]] -name = "scikit-learn" -version = "1.3.0" -description = "A set of python modules for machine learning and data mining" -optional = false -python-versions = ">=3.8" -files = [ - {file = "scikit-learn-1.3.0.tar.gz", hash = "sha256:8be549886f5eda46436b6e555b0e4873b4f10aa21c07df45c4bc1735afbccd7a"}, - {file = "scikit_learn-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:981287869e576d42c682cf7ca96af0c6ac544ed9316328fd0d9292795c742cf5"}, - {file = "scikit_learn-1.3.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:436aaaae2c916ad16631142488e4c82f4296af2404f480e031d866863425d2a2"}, - {file = "scikit_learn-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7e28d8fa47a0b30ae1bd7a079519dd852764e31708a7804da6cb6f8b36e3630"}, - {file = "scikit_learn-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae80c08834a473d08a204d966982a62e11c976228d306a2648c575e3ead12111"}, - {file = "scikit_learn-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:552fd1b6ee22900cf1780d7386a554bb96949e9a359999177cf30211e6b20df6"}, - {file = "scikit_learn-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79970a6d759eb00a62266a31e2637d07d2d28446fca8079cf9afa7c07b0427f8"}, - {file = "scikit_learn-1.3.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:850a00b559e636b23901aabbe79b73dc604b4e4248ba9e2d6e72f95063765603"}, - {file = "scikit_learn-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee04835fb016e8062ee9fe9074aef9b82e430504e420bff51e3e5fffe72750ca"}, - {file = "scikit_learn-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d953531f5d9f00c90c34fa3b7d7cfb43ecff4c605dac9e4255a20b114a27369"}, - {file = "scikit_learn-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:151ac2bf65ccf363664a689b8beafc9e6aae36263db114b4ca06fbbbf827444a"}, - {file = "scikit_learn-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a885a9edc9c0a341cab27ec4f8a6c58b35f3d449c9d2503a6fd23e06bbd4f6a"}, - {file = "scikit_learn-1.3.0-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:9877af9c6d1b15486e18a94101b742e9d0d2f343d35a634e337411ddb57783f3"}, - {file = "scikit_learn-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c470f53cea065ff3d588050955c492793bb50c19a92923490d18fcb637f6383a"}, - {file = "scikit_learn-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6e2d7389542eae01077a1ee0318c4fec20c66c957f45c7aac0c6eb0fe3c612"}, - {file = "scikit_learn-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:3a11936adbc379a6061ea32fa03338d4ca7248d86dd507c81e13af428a5bc1db"}, - {file = "scikit_learn-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:998d38fcec96584deee1e79cd127469b3ad6fefd1ea6c2dfc54e8db367eb396b"}, - {file = "scikit_learn-1.3.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:ded35e810438a527e17623ac6deae3b360134345b7c598175ab7741720d7ffa7"}, - {file = "scikit_learn-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e8102d5036e28d08ab47166b48c8d5e5810704daecf3a476a4282d562be9a28"}, - {file = "scikit_learn-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7617164951c422747e7c32be4afa15d75ad8044f42e7d70d3e2e0429a50e6718"}, - {file = "scikit_learn-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:1d54fb9e6038284548072df22fd34777e434153f7ffac72c8596f2d6987110dd"}, -] - [package.dependencies] joblib = ">=1.1.1" numpy = ">=1.17.3" @@ -2755,25 +2700,6 @@ dev = ["flake8", "mypy", "pycodestyle", "typing_extensions"] doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-panels (>=0.5.2)", "sphinx-tabs"] test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] -[[package]] -name = "send2trash" -version = "1.8.2" -description = "Send file to trash natively under Mac OS X, Windows and Linux" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "Send2Trash-1.8.2-py3-none-any.whl", hash = "sha256:a384719d99c07ce1eefd6905d2decb6f8b7ed054025bb0e618919f945de4f679"}, - {file = "Send2Trash-1.8.2.tar.gz", hash = "sha256:c132d59fa44b9ca2b1699af5c86f57ce9f4c5eb56629d5d55fbb7a35f84e2312"}, -] - -[package.dependencies] -numpy = ">=1.21.6,<1.28.0" - -[package.extras] -dev = ["click", "cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] -doc = ["jupytext", "matplotlib (>2)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] -test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] - [[package]] name = "six" version = "1.16.0" @@ -2785,6 +2711,17 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + [[package]] name = "soupsieve" version = "2.4.1" @@ -2923,6 +2860,23 @@ pure-eval = "*" [package.extras] tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] +[[package]] +name = "starlette" +version = "0.27.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.7" +files = [ + {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, + {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] + [[package]] name = "tabulate" version = "0.9.0" @@ -3132,4 +3086,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "3.10" -content-hash = "74593994c624e10c4b92a6598463374e1016717b4b1f12dbbe587b79b9ec4cef" +content-hash = "d664e94f27fbc28a6a366af80dbce68fecfffe66a7b3b15fb9cd5ce2c3a22b22" diff --git a/mini-projects/movie-rec-system/pyproject.toml b/mini-projects/movie-rec-system/pyproject.toml index d986041..54725c1 100644 --- a/mini-projects/movie-rec-system/pyproject.toml +++ b/mini-projects/movie-rec-system/pyproject.toml @@ -13,9 +13,11 @@ pandas = "2.0.1" matplotlib = "3.6.0" python-dotenv="1.0.0" ploomber = "0.22.4" -jupysql = "0.9.0" +jupysql = "0.10.0" duckdb-engine = "0.9.2" scikit-learn = "1.3.0" +fastapi = "^0.103.0" +pytest = "7.4.0" [build-system] From 11f37eb8f786219ee8e3f2d8ca9e3d5766b00a3d Mon Sep 17 00:00:00 2001 From: Laura Gutierrez Funderburk Date: Thu, 31 Aug 2023 20:42:59 -0700 Subject: [PATCH 06/14] add caching, add dependencies --- .../movie_rec_system/recommendation/app.py | 15 +++++---- .../recommendation/recommender.py | 11 +++---- mini-projects/movie-rec-system/poetry.lock | 32 ++++++++++++++++++- mini-projects/movie-rec-system/pyproject.toml | 1 + 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/app.py b/mini-projects/movie-rec-system/movie_rec_system/recommendation/app.py index 1db72a6..a7ced47 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/recommendation/app.py +++ b/mini-projects/movie-rec-system/movie_rec_system/recommendation/app.py @@ -1,14 +1,12 @@ -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException +from functools import lru_cache from recommender import get_recommendation app = FastAPI() - @app.get("/recommendations/") -def get_movie_recommendations( - movie: str, num_rec: int = 10, stop_words: str = "english" -): +def get_movie_recommendations(movie: str, num_rec: int = 10, stop_words: str = "english"): """ Get movie recommendations for a given movie. @@ -20,4 +18,9 @@ def get_movie_recommendations( Returns: JSON containing recommended movies and metrics. """ - return get_recommendation(movie, num_rec, stop_words) + recommendations = get_recommendation(movie, num_rec, stop_words) + + if not recommendations: + raise HTTPException(status_code=404, detail="Movie not found or no recommendations available") + + return recommendations diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py index 00a3783..76ba744 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py +++ b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py @@ -1,6 +1,7 @@ import json import pandas as pd import duckdb +from functools import lru_cache from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity from recommender_helper import ( @@ -11,13 +12,12 @@ ) +@lru_cache(maxsize=None) def get_data() -> pd.DataFrame: """ Function that automatically connects to duckdb as a GET call upon launch of FastAPI - - Returns a connection """ con = duckdb.connect("../../movies_data.duckdb") query = "SELECT * FROM movie_genre_data" @@ -126,7 +126,6 @@ def get_recommendation(movie: str, num_rec: int = 10, stop_words="english"): # Compute similarity similarity = cosine_similarity(tfidf_matrix) - similarity_df = pd.DataFrame( similarity, index=df.title.values, columns=df.title.values ) @@ -138,11 +137,12 @@ def get_recommendation(movie: str, num_rec: int = 10, stop_words="english"): movie, similarity_df, movie_list, num_rec ) + if not recommendations: + return None + # Compute metrics popularity_rmse = get_popularity_rmse(df, movie, recommendations) - vote_avg_rmse = get_vote_avg_rmse(df, movie, recommendations) - vote_count_rmse = get_vote_count_rmse(df, movie, recommendations) result = { @@ -156,5 +156,4 @@ def get_recommendation(movie: str, num_rec: int = 10, stop_words="english"): } result_json = json.dumps(result) - return result_json diff --git a/mini-projects/movie-rec-system/poetry.lock b/mini-projects/movie-rec-system/poetry.lock index 23122bb..6bf22d7 100644 --- a/mini-projects/movie-rec-system/poetry.lock +++ b/mini-projects/movie-rec-system/poetry.lock @@ -794,6 +794,17 @@ files = [ docs = ["Sphinx", "docutils (<0.18)"] test = ["objgraph", "psutil"] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + [[package]] name = "humanize" version = "4.8.0" @@ -3061,6 +3072,25 @@ secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17. socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "uvicorn" +version = "0.23.2" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"}, + {file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + [[package]] name = "wcwidth" version = "0.2.6" @@ -3086,4 +3116,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "3.10" -content-hash = "d664e94f27fbc28a6a366af80dbce68fecfffe66a7b3b15fb9cd5ce2c3a22b22" +content-hash = "4af60777b16a61a9b46dbfbe21ea1122a3a55e3eec848a8f3c926f1e7f008833" diff --git a/mini-projects/movie-rec-system/pyproject.toml b/mini-projects/movie-rec-system/pyproject.toml index 54725c1..542f1a3 100644 --- a/mini-projects/movie-rec-system/pyproject.toml +++ b/mini-projects/movie-rec-system/pyproject.toml @@ -18,6 +18,7 @@ duckdb-engine = "0.9.2" scikit-learn = "1.3.0" fastapi = "^0.103.0" pytest = "7.4.0" +uvicorn = "0.23.2" [build-system] From f43a0a4bedd5d8c887cfdb5b78d73f104c5cc59c Mon Sep 17 00:00:00 2001 From: Laura Gutierrez Funderburk Date: Thu, 31 Aug 2023 21:06:30 -0700 Subject: [PATCH 07/14] add robustness and pydantic model --- .../recommendation/__init__.py | 0 .../movie_rec_system/recommendation/app.py | 41 ++++++++++--- .../recommendation/recommender.py | 15 ++--- .../recommendation/recommender_helper.py | 59 ++++++++++--------- 4 files changed, 71 insertions(+), 44 deletions(-) create mode 100644 mini-projects/movie-rec-system/movie_rec_system/recommendation/__init__.py diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/__init__.py b/mini-projects/movie-rec-system/movie_rec_system/recommendation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/app.py b/mini-projects/movie-rec-system/movie_rec_system/recommendation/app.py index a7ced47..f27f95c 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/recommendation/app.py +++ b/mini-projects/movie-rec-system/movie_rec_system/recommendation/app.py @@ -1,12 +1,31 @@ from fastapi import FastAPI, HTTPException -from functools import lru_cache - +from pydantic import BaseModel, validator from recommender import get_recommendation app = FastAPI() -@app.get("/recommendations/") -def get_movie_recommendations(movie: str, num_rec: int = 10, stop_words: str = "english"): + +class RecommendationRequest(BaseModel): + movie: str + num_rec: int = 10 + stop_words: str = "english" + + @validator("movie", pre=True, always=True) + def format_movie_name(cls, movie_name): + """Ensure the movie name is formatted with the + first letter capitalized.""" + return movie_name.title() # Convert to title case + + +@app.get("/") +async def root(): + return { + "message": "Welcome! You can use this API to get movie recommendations based on viewers' votes. Visit /docs for more information and to try it out!" # noqa E501 + } + + +@app.post("/recommendations/") +def get_movie_recommendations(recommendation_request: RecommendationRequest): """ Get movie recommendations for a given movie. @@ -18,9 +37,17 @@ def get_movie_recommendations(movie: str, num_rec: int = 10, stop_words: str = " Returns: JSON containing recommended movies and metrics. """ - recommendations = get_recommendation(movie, num_rec, stop_words) - + recommendations = get_recommendation( + recommendation_request.movie, + recommendation_request.num_rec, + recommendation_request.stop_words, + ) + print(recommendations) + if not recommendations: - raise HTTPException(status_code=404, detail="Movie not found or no recommendations available") + raise HTTPException( + status_code=404, + detail="Movie not found or no recommendations available", # noqa E501 + ) return recommendations diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py index 76ba744..390d709 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py +++ b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py @@ -116,31 +116,26 @@ def get_recommendation(movie: str, num_rec: int = 10, stop_words="english"): """ df = get_data() + df["title"] = df["title"].str.lower() # Convert titles to lowercase once + movie = movie.lower() # Convert input movie to lowercase - # Create column with overview and genres df = create_combined(df) - - # Vectorize "combined" tfidf = TfidfVectorizer(stop_words=stop_words) tfidf_matrix = tfidf.fit_transform(df["combined"]) - # Compute similarity similarity = cosine_similarity(tfidf_matrix) + + # Make sure the dataframe's columns and indices are in lowercase similarity_df = pd.DataFrame( similarity, index=df.title.values, columns=df.title.values ) movie_list = similarity_df.columns.values - - # Get movie recommendations - recommendations = content_movie_recommender( - movie, similarity_df, movie_list, num_rec - ) + recommendations = content_movie_recommender(movie, similarity_df, movie_list, num_rec) if not recommendations: return None - # Compute metrics popularity_rmse = get_popularity_rmse(df, movie, recommendations) vote_avg_rmse = get_vote_avg_rmse(df, movie, recommendations) vote_count_rmse = get_vote_count_rmse(df, movie, recommendations) diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender_helper.py b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender_helper.py index e32f83f..422e0df 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender_helper.py +++ b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender_helper.py @@ -22,38 +22,43 @@ def content_movie_recommender( top_n : int number of similar movies to output """ - # movie list - movie_list = movie_database_list - - # get movie similarity records - movie_sim = similarity_database[ - similarity_database.index == input_movie - ].values[ # noqa E501 - 0 - ] - # get movies sorted by similarity - sorted_movie_ids = np.argsort(movie_sim)[::-1] - - # get recommended movie names - recommended_movies = movie_list[sorted_movie_ids[1 : top_n + 1]] # noqa E203 - - return list(recommended_movies) + try: + # get movie similarity records + movie_sim = similarity_database[ + similarity_database.index == input_movie + ].values[0] + + # get movies sorted by similarity + sorted_movie_ids = np.argsort(movie_sim)[::-1] + recommended_movies = movie_database_list[sorted_movie_ids[1 : top_n + 1]] + return list(recommended_movies) + except IndexError: + return [] def get_popularity_rmse( df: pd.DataFrame, sample_movie: str, recommendations: list ) -> float: - sample_movie_popularity = df[df["title"] == sample_movie].popularity.iloc[ - 0 - ] # noqa E501 - recommendations_popularity = df[ - df["title"].isin(recommendations) - ].popularity.values # noqa E501 - - squared_diffs = (sample_movie_popularity - recommendations_popularity) ** 2 - rmse = np.sqrt(squared_diffs.mean()) - - return round(float(rmse), 3) + # Convert titles in dataframe and sample_movie to lowercase + df["title"] = df["title"].str.lower() + sample_movie = sample_movie.lower() + + filtered_df = df[df["title"] == sample_movie] + + if not filtered_df.empty: + sample_movie_popularity = filtered_df.popularity.iloc[0] + recommendations_popularity = df[ + df["title"].isin(recommendations) + ].popularity.values + + squared_diffs = ( + sample_movie_popularity - recommendations_popularity + ) ** 2 # noqa E501 + rmse = np.sqrt(squared_diffs.mean()) + + return round(float(rmse), 3) + else: + return float("nan") def get_vote_avg_rmse( From 1509fede92acc1a4d2dc386dff94e01790bc595f Mon Sep 17 00:00:00 2001 From: Laura Gutierrez Funderburk Date: Thu, 31 Aug 2023 21:10:57 -0700 Subject: [PATCH 08/14] fix linting --- .../movie_rec_system/recommendation/recommender.py | 6 ++++-- .../movie_rec_system/recommendation/recommender_helper.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py index 390d709..5b990f7 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py +++ b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py @@ -124,14 +124,16 @@ def get_recommendation(movie: str, num_rec: int = 10, stop_words="english"): tfidf_matrix = tfidf.fit_transform(df["combined"]) similarity = cosine_similarity(tfidf_matrix) - + # Make sure the dataframe's columns and indices are in lowercase similarity_df = pd.DataFrame( similarity, index=df.title.values, columns=df.title.values ) movie_list = similarity_df.columns.values - recommendations = content_movie_recommender(movie, similarity_df, movie_list, num_rec) + recommendations = content_movie_recommender( + movie, similarity_df, movie_list, num_rec + ) if not recommendations: return None diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender_helper.py b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender_helper.py index 422e0df..caefa66 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender_helper.py +++ b/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender_helper.py @@ -27,10 +27,12 @@ def content_movie_recommender( movie_sim = similarity_database[ similarity_database.index == input_movie ].values[0] - + # get movies sorted by similarity sorted_movie_ids = np.argsort(movie_sim)[::-1] - recommended_movies = movie_database_list[sorted_movie_ids[1 : top_n + 1]] + recommended_movies = movie_database_list[ + sorted_movie_ids[1 : top_n + 1] # noqa E203 + ] # noqa E501 return list(recommended_movies) except IndexError: return [] From 7033c5ca625172e827e08f2495f621a93bb59742 Mon Sep 17 00:00:00 2001 From: Laura Gutierrez Funderburk Date: Thu, 31 Aug 2023 21:26:18 -0700 Subject: [PATCH 09/14] directory structure fix, fix relative imports, enable execution from project root --- .../movie_rec_system/{recommendation => app}/__init__.py | 0 .../movie_rec_system/{recommendation => app}/app.py | 2 +- .../movie_rec_system/{recommendation => app}/recommender.py | 4 ++-- .../recommender_helper.py => app/recommenderhelper.py} | 0 .../{recommendation => notebooks}/recommender.ipynb | 0 5 files changed, 3 insertions(+), 3 deletions(-) rename mini-projects/movie-rec-system/movie_rec_system/{recommendation => app}/__init__.py (100%) rename mini-projects/movie-rec-system/movie_rec_system/{recommendation => app}/app.py (97%) rename mini-projects/movie-rec-system/movie_rec_system/{recommendation => app}/recommender.py (98%) rename mini-projects/movie-rec-system/movie_rec_system/{recommendation/recommender_helper.py => app/recommenderhelper.py} (100%) rename mini-projects/movie-rec-system/movie_rec_system/{recommendation => notebooks}/recommender.ipynb (100%) diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/__init__.py b/mini-projects/movie-rec-system/movie_rec_system/app/__init__.py similarity index 100% rename from mini-projects/movie-rec-system/movie_rec_system/recommendation/__init__.py rename to mini-projects/movie-rec-system/movie_rec_system/app/__init__.py diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/app.py b/mini-projects/movie-rec-system/movie_rec_system/app/app.py similarity index 97% rename from mini-projects/movie-rec-system/movie_rec_system/recommendation/app.py rename to mini-projects/movie-rec-system/movie_rec_system/app/app.py index f27f95c..5d2c2f3 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/recommendation/app.py +++ b/mini-projects/movie-rec-system/movie_rec_system/app/app.py @@ -1,6 +1,6 @@ from fastapi import FastAPI, HTTPException from pydantic import BaseModel, validator -from recommender import get_recommendation +from .recommender import get_recommendation app = FastAPI() diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py b/mini-projects/movie-rec-system/movie_rec_system/app/recommender.py similarity index 98% rename from mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py rename to mini-projects/movie-rec-system/movie_rec_system/app/recommender.py index 5b990f7..902d8fe 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.py +++ b/mini-projects/movie-rec-system/movie_rec_system/app/recommender.py @@ -4,7 +4,7 @@ from functools import lru_cache from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity -from recommender_helper import ( +from .recommenderhelper import ( content_movie_recommender, get_popularity_rmse, get_vote_avg_rmse, @@ -19,7 +19,7 @@ def get_data() -> pd.DataFrame: to duckdb as a GET call upon launch of FastAPI """ - con = duckdb.connect("../../movies_data.duckdb") + con = duckdb.connect("./movies_data.duckdb") query = "SELECT * FROM movie_genre_data" df = con.execute(query).fetchdf() con.close() diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender_helper.py b/mini-projects/movie-rec-system/movie_rec_system/app/recommenderhelper.py similarity index 100% rename from mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender_helper.py rename to mini-projects/movie-rec-system/movie_rec_system/app/recommenderhelper.py diff --git a/mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.ipynb b/mini-projects/movie-rec-system/movie_rec_system/notebooks/recommender.ipynb similarity index 100% rename from mini-projects/movie-rec-system/movie_rec_system/recommendation/recommender.ipynb rename to mini-projects/movie-rec-system/movie_rec_system/notebooks/recommender.ipynb From bfe610240c6bfeaca12534c037dd77110467a3a9 Mon Sep 17 00:00:00 2001 From: Laura Gutierrez Funderburk Date: Thu, 31 Aug 2023 21:41:05 -0700 Subject: [PATCH 10/14] set up test suite --- .../movie_rec_system/app/app.py | 9 +++- mini-projects/movie-rec-system/poetry.lock | 46 ++++++++++++++++++- mini-projects/movie-rec-system/pyproject.toml | 1 + .../movie-rec-system/tests/test_app.py | 30 ++++++++++++ 4 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 mini-projects/movie-rec-system/tests/test_app.py diff --git a/mini-projects/movie-rec-system/movie_rec_system/app/app.py b/mini-projects/movie-rec-system/movie_rec_system/app/app.py index 5d2c2f3..8a4b44e 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/app/app.py +++ b/mini-projects/movie-rec-system/movie_rec_system/app/app.py @@ -1,6 +1,8 @@ from fastapi import FastAPI, HTTPException from pydantic import BaseModel, validator from .recommender import get_recommendation +from fastapi.responses import JSONResponse +import json app = FastAPI() @@ -42,7 +44,10 @@ def get_movie_recommendations(recommendation_request: RecommendationRequest): recommendation_request.num_rec, recommendation_request.stop_words, ) - print(recommendations) + + if isinstance(recommendations, str): + recommendations = json.loads(recommendations) + if not recommendations: raise HTTPException( @@ -50,4 +55,4 @@ def get_movie_recommendations(recommendation_request: RecommendationRequest): detail="Movie not found or no recommendations available", # noqa E501 ) - return recommendations + return JSONResponse(content=recommendations) diff --git a/mini-projects/movie-rec-system/poetry.lock b/mini-projects/movie-rec-system/poetry.lock index 6bf22d7..de7963b 100644 --- a/mini-projects/movie-rec-system/poetry.lock +++ b/mini-projects/movie-rec-system/poetry.lock @@ -805,6 +805,50 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "httpcore" +version = "0.17.3" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"}, + {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"}, +] + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = "==1.*" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "httpx" +version = "0.24.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, + {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, +] + +[package.dependencies] +certifi = "*" +httpcore = ">=0.15.0,<0.18.0" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + [[package]] name = "humanize" version = "4.8.0" @@ -3116,4 +3160,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "3.10" -content-hash = "4af60777b16a61a9b46dbfbe21ea1122a3a55e3eec848a8f3c926f1e7f008833" +content-hash = "1b30aada5b95507ef134e0728e0fe3871b23407ad25806dbd27a3c88a1806029" diff --git a/mini-projects/movie-rec-system/pyproject.toml b/mini-projects/movie-rec-system/pyproject.toml index 542f1a3..21f6b09 100644 --- a/mini-projects/movie-rec-system/pyproject.toml +++ b/mini-projects/movie-rec-system/pyproject.toml @@ -19,6 +19,7 @@ scikit-learn = "1.3.0" fastapi = "^0.103.0" pytest = "7.4.0" uvicorn = "0.23.2" +httpx = "^0.24.1" [build-system] diff --git a/mini-projects/movie-rec-system/tests/test_app.py b/mini-projects/movie-rec-system/tests/test_app.py new file mode 100644 index 0000000..325fc80 --- /dev/null +++ b/mini-projects/movie-rec-system/tests/test_app.py @@ -0,0 +1,30 @@ +from fastapi.testclient import TestClient +from movie_rec_system.app.app import app + +client = TestClient(app) + +def test_root_endpoint(): + response = client.get("/") + assert response.status_code == 200 + assert response.json() == { + "message": "Welcome! You can use this API to get movie recommendations based on viewers' votes. Visit /docs for more information and to try it out!" + } + +def test_recommendation_endpoint(): + test_data = { + "movie": "Inception", + "num_rec": 5 + } + response = client.post("/recommendations/", json=test_data) + assert response.status_code == 200 + + response_data = response.json() + assert response_data["movie"] == "inception" + +def test_recommendation_for_nonexistent_movie(): + test_data = { + "movie": "NonExistentMovie", + "num_rec": 5 + } + response = client.post("/recommendations/", json=test_data) + assert response.status_code == 404 From 2b7adaf39cc1529f8ec5d7e1cb3ccbcc512a43a0 Mon Sep 17 00:00:00 2001 From: Laura Gutierrez Funderburk Date: Thu, 31 Aug 2023 21:42:08 -0700 Subject: [PATCH 11/14] fix linting --- .../movie_rec_system/app/app.py | 3 +-- .../movie-rec-system/tests/test_app.py | 19 ++++++++----------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/mini-projects/movie-rec-system/movie_rec_system/app/app.py b/mini-projects/movie-rec-system/movie_rec_system/app/app.py index 8a4b44e..fb3c38b 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/app/app.py +++ b/mini-projects/movie-rec-system/movie_rec_system/app/app.py @@ -2,7 +2,7 @@ from pydantic import BaseModel, validator from .recommender import get_recommendation from fastapi.responses import JSONResponse -import json +import json app = FastAPI() @@ -48,7 +48,6 @@ def get_movie_recommendations(recommendation_request: RecommendationRequest): if isinstance(recommendations, str): recommendations = json.loads(recommendations) - if not recommendations: raise HTTPException( status_code=404, diff --git a/mini-projects/movie-rec-system/tests/test_app.py b/mini-projects/movie-rec-system/tests/test_app.py index 325fc80..e9f7a4c 100644 --- a/mini-projects/movie-rec-system/tests/test_app.py +++ b/mini-projects/movie-rec-system/tests/test_app.py @@ -3,28 +3,25 @@ client = TestClient(app) + def test_root_endpoint(): response = client.get("/") assert response.status_code == 200 assert response.json() == { - "message": "Welcome! You can use this API to get movie recommendations based on viewers' votes. Visit /docs for more information and to try it out!" + "message": "Welcome! You can use this API to get movie recommendations based on viewers' votes. Visit /docs for more information and to try it out!" # noqa E501 } + def test_recommendation_endpoint(): - test_data = { - "movie": "Inception", - "num_rec": 5 - } + test_data = {"movie": "Inception", "num_rec": 5} response = client.post("/recommendations/", json=test_data) assert response.status_code == 200 - + response_data = response.json() - assert response_data["movie"] == "inception" + assert response_data["movie"] == "inception" + def test_recommendation_for_nonexistent_movie(): - test_data = { - "movie": "NonExistentMovie", - "num_rec": 5 - } + test_data = {"movie": "NonExistentMovie", "num_rec": 5} response = client.post("/recommendations/", json=test_data) assert response.status_code == 404 From 66fc82c76eb504d97a06ab7406e5f6cf1ffcee5d Mon Sep 17 00:00:00 2001 From: Laura Gutierrez Funderburk Date: Thu, 31 Aug 2023 22:01:32 -0700 Subject: [PATCH 12/14] modularize recommender.py - break functions down into smaller more specialized units --- .../movie_rec_system/app/recommender.py | 95 ++++++++++++++++--- 1 file changed, 83 insertions(+), 12 deletions(-) diff --git a/mini-projects/movie-rec-system/movie_rec_system/app/recommender.py b/mini-projects/movie-rec-system/movie_rec_system/app/recommender.py index 902d8fe..b740e82 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/app/recommender.py +++ b/mini-projects/movie-rec-system/movie_rec_system/app/recommender.py @@ -65,6 +65,83 @@ def create_combined(df: pd.DataFrame, weight=2) -> pd.DataFrame: return df +def retrieve_and_transform_data() -> pd.DataFrame: + """ + Retrieve data from duckdb and transform it + into a format that can be used for generating + movie recommendations. + + Returns + ------- + pd.DataFrame + The transformed DataFrame with an additional "combined" column. + """ + df = get_data() + df["title"] = df["title"].str.lower() + df = create_combined(df) + return df + + +def compute_tfidf_vectorization(df, stop_words="english"): + """ + Compute TF-IDF vectorization of the "combined" column + in the provided DataFrame. + + Parameters + ---------- + df : pd.DataFrame + The input DataFrame which must contain + a "combined" column. + + stop_words : str, optional + The language of stop words to be + used when vectorizing the "combined" column. + Default is "english". + + Returns + ------- + tfidf_matrix: scipy.sparse.csr.csr_matrix + The TF-IDF vectorization of the "combined" column.""" + tfidf = TfidfVectorizer(stop_words=stop_words) + tfidf_matrix = tfidf.fit_transform(df["combined"]) + return tfidf_matrix + + +def compute_metrics(df, movie, recommendations): + """ + Compute RMSE for popularity, vote average, and vote count + for the provided movie and recommendations. + + Parameters + ---------- + df : pd.DataFrame + The input DataFrame which must contain + a "combined" column. + + movie : str + The title of the movie for which + recommendations are to be generated. + + recommendations : list + A list of recommended movies. + + Returns + ------- + popularity_rmse : float + The RMSE for popularity. + + vote_avg_rmse : float + The RMSE for vote average. + + ote_count_rmse : float + The RMSE for vote count. + """ + popularity_rmse = get_popularity_rmse(df, movie, recommendations) + vote_avg_rmse = get_vote_avg_rmse(df, movie, recommendations) + vote_count_rmse = get_vote_count_rmse(df, movie, recommendations) + return popularity_rmse, vote_avg_rmse, vote_count_rmse + + def get_recommendation(movie: str, num_rec: int = 10, stop_words="english"): """ Generate movie recommendations based on @@ -115,21 +192,15 @@ def get_recommendation(movie: str, num_rec: int = 10, stop_words="english"): } """ - df = get_data() - df["title"] = df["title"].str.lower() # Convert titles to lowercase once - movie = movie.lower() # Convert input movie to lowercase - - df = create_combined(df) - tfidf = TfidfVectorizer(stop_words=stop_words) - tfidf_matrix = tfidf.fit_transform(df["combined"]) + movie = movie.lower() + df = retrieve_and_transform_data() + tfidf_matrix = compute_tfidf_vectorization(df, stop_words) similarity = cosine_similarity(tfidf_matrix) - # Make sure the dataframe's columns and indices are in lowercase similarity_df = pd.DataFrame( similarity, index=df.title.values, columns=df.title.values ) - movie_list = similarity_df.columns.values recommendations = content_movie_recommender( movie, similarity_df, movie_list, num_rec @@ -138,9 +209,9 @@ def get_recommendation(movie: str, num_rec: int = 10, stop_words="english"): if not recommendations: return None - popularity_rmse = get_popularity_rmse(df, movie, recommendations) - vote_avg_rmse = get_vote_avg_rmse(df, movie, recommendations) - vote_count_rmse = get_vote_count_rmse(df, movie, recommendations) + popularity_rmse, vote_avg_rmse, vote_count_rmse = compute_metrics( + df, movie, recommendations + ) result = { "movie": movie, From e123afbf3690d380b6f13b13dc4f16d76d54b24a Mon Sep 17 00:00:00 2001 From: Laura Gutierrez Funderburk Date: Thu, 31 Aug 2023 22:08:46 -0700 Subject: [PATCH 13/14] remove stop words from pydantic model --- mini-projects/movie-rec-system/movie_rec_system/app/app.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mini-projects/movie-rec-system/movie_rec_system/app/app.py b/mini-projects/movie-rec-system/movie_rec_system/app/app.py index fb3c38b..80ee0d3 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/app/app.py +++ b/mini-projects/movie-rec-system/movie_rec_system/app/app.py @@ -10,7 +10,6 @@ class RecommendationRequest(BaseModel): movie: str num_rec: int = 10 - stop_words: str = "english" @validator("movie", pre=True, always=True) def format_movie_name(cls, movie_name): @@ -34,7 +33,6 @@ def get_movie_recommendations(recommendation_request: RecommendationRequest): Parameters: - movie: The name of the movie for which you want recommendations. - num_rec: The number of movie recommendations you want. Default is 10. - - stop_words: The language for stop words. Default is "english". Returns: JSON containing recommended movies and metrics. @@ -42,7 +40,7 @@ def get_movie_recommendations(recommendation_request: RecommendationRequest): recommendations = get_recommendation( recommendation_request.movie, recommendation_request.num_rec, - recommendation_request.stop_words, + "english", ) if isinstance(recommendations, str): From 91b87a4d774795adaadbc2a3adaf80500bf8abbd Mon Sep 17 00:00:00 2001 From: jpjon Date: Fri, 1 Sep 2023 12:02:34 -0700 Subject: [PATCH 14/14] fixed warnings by upgrading pandas in poetry, created test case for given movie to return desired result --- .../movie_rec_system/app/app.py | 4 +- mini-projects/movie-rec-system/poetry.lock | 178 +++++++++--------- mini-projects/movie-rec-system/pyproject.toml | 2 +- .../movie-rec-system/tests/test_app.py | 24 +++ 4 files changed, 116 insertions(+), 92 deletions(-) diff --git a/mini-projects/movie-rec-system/movie_rec_system/app/app.py b/mini-projects/movie-rec-system/movie_rec_system/app/app.py index 80ee0d3..ed383bb 100644 --- a/mini-projects/movie-rec-system/movie_rec_system/app/app.py +++ b/mini-projects/movie-rec-system/movie_rec_system/app/app.py @@ -1,5 +1,5 @@ from fastapi import FastAPI, HTTPException -from pydantic import BaseModel, validator +from pydantic import BaseModel, field_validator from .recommender import get_recommendation from fastapi.responses import JSONResponse import json @@ -11,7 +11,7 @@ class RecommendationRequest(BaseModel): movie: str num_rec: int = 10 - @validator("movie", pre=True, always=True) + @field_validator("movie") def format_movie_name(cls, movie_name): """Ensure the movie name is formatted with the first letter capitalized.""" diff --git a/mini-projects/movie-rec-system/poetry.lock b/mini-projects/movie-rec-system/poetry.lock index de7963b..916166a 100644 --- a/mini-projects/movie-rec-system/poetry.lock +++ b/mini-projects/movie-rec-system/poetry.lock @@ -936,13 +936,13 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio" [[package]] name = "ipython" -version = "8.14.0" +version = "8.15.0" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.9" files = [ - {file = "ipython-8.14.0-py3-none-any.whl", hash = "sha256:248aca623f5c99a6635bc3857677b7320b9b8039f99f070ee0d20a5ca5a8e6bf"}, - {file = "ipython-8.14.0.tar.gz", hash = "sha256:1d197b907b6ba441b692c48cf2a3a2de280dc0ac91a3405b39349a50272ca0a1"}, + {file = "ipython-8.15.0-py3-none-any.whl", hash = "sha256:45a2c3a529296870a97b7de34eda4a31bee16bc7bf954e07d39abe49caf8f887"}, + {file = "ipython-8.15.0.tar.gz", hash = "sha256:2baeb5be6949eeebf532150f81746f8333e2ccce02de1c7eedde3f23ed5e9f1e"}, ] [package.dependencies] @@ -950,6 +950,7 @@ appnope = {version = "*", markers = "sys_platform == \"darwin\""} backcall = "*" colorama = {version = "*", markers = "sys_platform == \"win32\""} decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} jedi = ">=0.16" matplotlib-inline = "*" pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} @@ -960,9 +961,9 @@ stack-data = "*" traitlets = ">=5" [package.extras] -all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] black = ["black"] -doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] nbformat = ["nbformat"] @@ -1092,13 +1093,13 @@ integration = ["black", "dockerctx", "duckdb (<0.8.0)", "duckdb-engine", "flake8 [[package]] name = "jupyter-client" -version = "8.3.0" +version = "8.3.1" description = "Jupyter protocol implementation and client libraries" optional = false python-versions = ">=3.8" files = [ - {file = "jupyter_client-8.3.0-py3-none-any.whl", hash = "sha256:7441af0c0672edc5d28035e92ba5e32fadcfa8a4e608a434c228836a89df6158"}, - {file = "jupyter_client-8.3.0.tar.gz", hash = "sha256:3af69921fe99617be1670399a0b857ad67275eefcfa291e2c81a160b7b650f5f"}, + {file = "jupyter_client-8.3.1-py3-none-any.whl", hash = "sha256:5eb9f55eb0650e81de6b7e34308d8b92d04fe4ec41cd8193a913979e33d8e1a5"}, + {file = "jupyter_client-8.3.1.tar.gz", hash = "sha256:60294b2d5b869356c893f57b1a877ea6510d60d45cf4b38057f1672d85699ac9"}, ] [package.dependencies] @@ -1512,13 +1513,13 @@ test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>= [[package]] name = "nbconvert" -version = "7.7.4" +version = "7.8.0" description = "Converting Jupyter Notebooks" optional = false python-versions = ">=3.8" files = [ - {file = "nbconvert-7.7.4-py3-none-any.whl", hash = "sha256:ace26f4386d08eb5c55833596a942048c5502a95e05590cb523826a749a40a37"}, - {file = "nbconvert-7.7.4.tar.gz", hash = "sha256:1113d039fa3fc3a846ffa5a3b0a019e85aaa94c566a09fa0c400fb7638e46087"}, + {file = "nbconvert-7.8.0-py3-none-any.whl", hash = "sha256:aec605e051fa682ccc7934ccc338ba1e8b626cfadbab0db592106b630f63f0f2"}, + {file = "nbconvert-7.8.0.tar.gz", hash = "sha256:f5bc15a1247e14dd41ceef0c0a3bc70020e016576eb0578da62f1c5b4f950479"}, ] [package.dependencies] @@ -1644,66 +1645,61 @@ files = [ [[package]] name = "pandas" -version = "2.0.1" +version = "2.1.0" description = "Powerful data structures for data analysis, time series, and statistics" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pandas-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70a996a1d2432dadedbb638fe7d921c88b0cc4dd90374eab51bb33dc6c0c2a12"}, - {file = "pandas-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:909a72b52175590debbf1d0c9e3e6bce2f1833c80c76d80bd1aa09188be768e5"}, - {file = "pandas-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe7914d8ddb2d54b900cec264c090b88d141a1eed605c9539a187dbc2547f022"}, - {file = "pandas-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a514ae436b23a92366fbad8365807fc0eed15ca219690b3445dcfa33597a5cc"}, - {file = "pandas-2.0.1-cp310-cp310-win32.whl", hash = "sha256:12bd6618e3cc737c5200ecabbbb5eaba8ab645a4b0db508ceeb4004bb10b060e"}, - {file = "pandas-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:2b6fe5f7ce1cba0e74188c8473c9091ead9b293ef0a6794939f8cc7947057abd"}, - {file = "pandas-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:00959a04a1d7bbc63d75a768540fb20ecc9e65fd80744c930e23768345a362a7"}, - {file = "pandas-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af2449e9e984dfad39276b885271ba31c5e0204ffd9f21f287a245980b0e4091"}, - {file = "pandas-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:910df06feaf9935d05247db6de452f6d59820e432c18a2919a92ffcd98f8f79b"}, - {file = "pandas-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fa0067f2419f933101bdc6001bcea1d50812afbd367b30943417d67fbb99678"}, - {file = "pandas-2.0.1-cp311-cp311-win32.whl", hash = "sha256:7b8395d335b08bc8b050590da264f94a439b4770ff16bb51798527f1dd840388"}, - {file = "pandas-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:8db5a644d184a38e6ed40feeb12d410d7fcc36648443defe4707022da127fc35"}, - {file = "pandas-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7bbf173d364130334e0159a9a034f573e8b44a05320995127cf676b85fd8ce86"}, - {file = "pandas-2.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c0853d487b6c868bf107a4b270a823746175b1932093b537b9b76c639fc6f7e"}, - {file = "pandas-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25e23a03f7ad7211ffa30cb181c3e5f6d96a8e4cb22898af462a7333f8a74eb"}, - {file = "pandas-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e09a53a4fe8d6ae2149959a2d02e1ef2f4d2ceb285ac48f74b79798507e468b4"}, - {file = "pandas-2.0.1-cp38-cp38-win32.whl", hash = "sha256:a2564629b3a47b6aa303e024e3d84e850d36746f7e804347f64229f8c87416ea"}, - {file = "pandas-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:03e677c6bc9cfb7f93a8b617d44f6091613a5671ef2944818469be7b42114a00"}, - {file = "pandas-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3d099ecaa5b9e977b55cd43cf842ec13b14afa1cfa51b7e1179d90b38c53ce6a"}, - {file = "pandas-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a37ee35a3eb6ce523b2c064af6286c45ea1c7ff882d46e10d0945dbda7572753"}, - {file = "pandas-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:320b180d125c3842c5da5889183b9a43da4ebba375ab2ef938f57bf267a3c684"}, - {file = "pandas-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18d22cb9043b6c6804529810f492ab09d638ddf625c5dea8529239607295cb59"}, - {file = "pandas-2.0.1-cp39-cp39-win32.whl", hash = "sha256:90d1d365d77d287063c5e339f49b27bd99ef06d10a8843cf00b1a49326d492c1"}, - {file = "pandas-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:99f7192d8b0e6daf8e0d0fd93baa40056684e4b4aaaef9ea78dff34168e1f2f0"}, - {file = "pandas-2.0.1.tar.gz", hash = "sha256:19b8e5270da32b41ebf12f0e7165efa7024492e9513fb46fb631c5022ae5709d"}, + {file = "pandas-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40dd20439ff94f1b2ed55b393ecee9cb6f3b08104c2c40b0cb7186a2f0046242"}, + {file = "pandas-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4f38e4fedeba580285eaac7ede4f686c6701a9e618d8a857b138a126d067f2f"}, + {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e6a0fe052cf27ceb29be9429428b4918f3740e37ff185658f40d8702f0b3e09"}, + {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d81e1813191070440d4c7a413cb673052b3b4a984ffd86b8dd468c45742d3cc"}, + {file = "pandas-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb20252720b1cc1b7d0b2879ffc7e0542dd568f24d7c4b2347cb035206936421"}, + {file = "pandas-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:38f74ef7ebc0ffb43b3d633e23d74882bce7e27bfa09607f3c5d3e03ffd9a4a5"}, + {file = "pandas-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cda72cc8c4761c8f1d97b169661f23a86b16fdb240bdc341173aee17e4d6cedd"}, + {file = "pandas-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d97daeac0db8c993420b10da4f5f5b39b01fc9ca689a17844e07c0a35ac96b4b"}, + {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8c58b1113892e0c8078f006a167cc210a92bdae23322bb4614f2f0b7a4b510f"}, + {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:629124923bcf798965b054a540f9ccdfd60f71361255c81fa1ecd94a904b9dd3"}, + {file = "pandas-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:70cf866af3ab346a10debba8ea78077cf3a8cd14bd5e4bed3d41555a3280041c"}, + {file = "pandas-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d53c8c1001f6a192ff1de1efe03b31a423d0eee2e9e855e69d004308e046e694"}, + {file = "pandas-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:86f100b3876b8c6d1a2c66207288ead435dc71041ee4aea789e55ef0e06408cb"}, + {file = "pandas-2.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28f330845ad21c11db51e02d8d69acc9035edfd1116926ff7245c7215db57957"}, + {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9a6ccf0963db88f9b12df6720e55f337447aea217f426a22d71f4213a3099a6"}, + {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99e678180bc59b0c9443314297bddce4ad35727a1a2656dbe585fd78710b3b9"}, + {file = "pandas-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b31da36d376d50a1a492efb18097b9101bdbd8b3fbb3f49006e02d4495d4c644"}, + {file = "pandas-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0164b85937707ec7f70b34a6c3a578dbf0f50787f910f21ca3b26a7fd3363437"}, + {file = "pandas-2.1.0.tar.gz", hash = "sha256:62c24c7fc59e42b775ce0679cfa7b14a5f9bfb7643cfbe708c960699e05fb918"}, ] [package.dependencies] -numpy = {version = ">=1.21.0", markers = "python_version >= \"3.10\""} +numpy = {version = ">=1.22.4", markers = "python_version < \"3.11\""} python-dateutil = ">=2.8.2" pytz = ">=2020.1" tzdata = ">=2022.1" [package.extras] -all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"] -aws = ["s3fs (>=2021.08.0)"] -clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"] -compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"] -computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"] +all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"] +aws = ["s3fs (>=2022.05.0)"] +clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"] +compression = ["zstandard (>=0.17.0)"] +computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"] feather = ["pyarrow (>=7.0.0)"] -fss = ["fsspec (>=2021.07.0)"] -gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"] -hdf5 = ["tables (>=3.6.1)"] -html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"] -mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"] +fss = ["fsspec (>=2022.05.0)"] +gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"] +hdf5 = ["tables (>=3.7.0)"] +html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"] +mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"] parquet = ["pyarrow (>=7.0.0)"] -performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"] +performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"] plot = ["matplotlib (>=3.6.1)"] -postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"] -spss = ["pyreadstat (>=1.1.2)"] -sql-other = ["SQLAlchemy (>=1.4.16)"] -test = ["hypothesis (>=6.34.2)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.6.3)"] +postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"] +spss = ["pyreadstat (>=1.1.5)"] +sql-other = ["SQLAlchemy (>=1.4.36)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.8.0)"] [[package]] name = "pandocfilters" @@ -2719,41 +2715,45 @@ tests = ["black (>=23.3.0)", "matplotlib (>=3.1.3)", "mypy (>=1.3)", "numpydoc ( [[package]] name = "scipy" -version = "1.9.3" +version = "1.11.2" description = "Fundamental algorithms for scientific computing in Python" optional = false -python-versions = ">=3.8" -files = [ - {file = "scipy-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0"}, - {file = "scipy-1.9.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9"}, - {file = "scipy-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523"}, - {file = "scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b41bc822679ad1c9a5f023bc93f6d0543129ca0f37c1ce294dd9d386f0a21096"}, - {file = "scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:90453d2b93ea82a9f434e4e1cba043e779ff67b92f7a0e85d05d286a3625df3c"}, - {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c06e62a390a9167da60bedd4575a14c1f58ca9dfde59830fc42e5197283dab"}, - {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abaf921531b5aeaafced90157db505e10345e45038c39e5d9b6c7922d68085cb"}, - {file = "scipy-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:06d2e1b4c491dc7d8eacea139a1b0b295f74e1a1a0f704c375028f8320d16e31"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc"}, - {file = "scipy-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0"}, - {file = "scipy-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58"}, - {file = "scipy-1.9.3.tar.gz", hash = "sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027"}, +python-versions = "<3.13,>=3.9" +files = [ + {file = "scipy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2b997a5369e2d30c97995dcb29d638701f8000d04df01b8e947f206e5d0ac788"}, + {file = "scipy-1.11.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:95763fbda1206bec41157582bea482f50eb3702c85fffcf6d24394b071c0e87a"}, + {file = "scipy-1.11.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e367904a0fec76433bf3fbf3e85bf60dae8e9e585ffd21898ab1085a29a04d16"}, + {file = "scipy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d690e1ca993c8f7ede6d22e5637541217fc6a4d3f78b3672a6fe454dbb7eb9a7"}, + {file = "scipy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d2b813bfbe8dec6a75164523de650bad41f4405d35b0fa24c2c28ae07fcefb20"}, + {file = "scipy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:afdb0d983f6135d50770dd979df50bf1c7f58b5b33e0eb8cf5c73c70600eae1d"}, + {file = "scipy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d9886f44ef8c9e776cb7527fb01455bf4f4a46c455c4682edc2c2cc8cd78562"}, + {file = "scipy-1.11.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1342ca385c673208f32472830c10110a9dcd053cf0c4b7d4cd7026d0335a6c1d"}, + {file = "scipy-1.11.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b133f237bd8ba73bad51bc12eb4f2d84cbec999753bf25ba58235e9fc2096d80"}, + {file = "scipy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aeb87661de987f8ec56fa6950863994cd427209158255a389fc5aea51fa7055"}, + {file = "scipy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:90d3b1364e751d8214e325c371f0ee0dd38419268bf4888b2ae1040a6b266b2a"}, + {file = "scipy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:f73102f769ee06041a3aa26b5841359b1a93cc364ce45609657751795e8f4a4a"}, + {file = "scipy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa4909c6c20c3d91480533cddbc0e7c6d849e7d9ded692918c76ce5964997898"}, + {file = "scipy-1.11.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ac74b1512d38718fb6a491c439aa7b3605b96b1ed3be6599c17d49d6c60fca18"}, + {file = "scipy-1.11.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8425fa963a32936c9773ee3ce44a765d8ff67eed5f4ac81dc1e4a819a238ee9"}, + {file = "scipy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:542a757e2a6ec409e71df3d8fd20127afbbacb1c07990cb23c5870c13953d899"}, + {file = "scipy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ea932570b1c2a30edafca922345854ff2cd20d43cd9123b6dacfdecebfc1a80b"}, + {file = "scipy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:4447ad057d7597476f9862ecbd9285bbf13ba9d73ce25acfa4e4b11c6801b4c9"}, + {file = "scipy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b0620240ef445b5ddde52460e6bc3483b7c9c750275369379e5f609a1050911c"}, + {file = "scipy-1.11.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:f28f1f6cfeb48339c192efc6275749b2a25a7e49c4d8369a28b6591da02fbc9a"}, + {file = "scipy-1.11.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:214cdf04bbae7a54784f8431f976704ed607c4bc69ba0d5d5d6a9df84374df76"}, + {file = "scipy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10eb6af2f751aa3424762948e5352f707b0dece77288206f227864ddf675aca0"}, + {file = "scipy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0f3261f14b767b316d7137c66cc4f33a80ea05841b9c87ad83a726205b901423"}, + {file = "scipy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:2c91cf049ffb5575917f2a01da1da082fd24ed48120d08a6e7297dfcac771dcd"}, + {file = "scipy-1.11.2.tar.gz", hash = "sha256:b29318a5e39bd200ca4381d80b065cdf3076c7d7281c5e36569e99273867f61d"}, ] [package.dependencies] -numpy = ">=1.18.5,<1.26.0" +numpy = ">=1.21.6,<1.28.0" [package.extras] -dev = ["flake8", "mypy", "pycodestyle", "typing_extensions"] -doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-panels (>=0.5.2)", "sphinx-tabs"] -test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +dev = ["click", "cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] +doc = ["jupytext", "matplotlib (>2)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] +test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [[package]] name = "six" @@ -2868,13 +2868,13 @@ sqlcipher = ["sqlcipher3-binary"] [[package]] name = "sqlglot" -version = "17.16.2" +version = "18.0.1" description = "An easily customizable SQL parser and transpiler" optional = false python-versions = "*" files = [ - {file = "sqlglot-17.16.2-py3-none-any.whl", hash = "sha256:c7b5bedd5aaa4a861caa73dadc7f31d68811fb8d7e197563f160b1935879de7a"}, - {file = "sqlglot-17.16.2.tar.gz", hash = "sha256:a3447e81da039e8fd3741ca9021141ae51f6909f1de43c18a93a679160538549"}, + {file = "sqlglot-18.0.1-py3-none-any.whl", hash = "sha256:18c85a87e10c1ba23e574350ad51cc4c557564edcf72d3d3194e2e72acc814db"}, + {file = "sqlglot-18.0.1.tar.gz", hash = "sha256:c4fd00c1026ddceb0f699271f68c6f792fbd36be6dd6101b2b8868838abfaa65"}, ] [package.extras] @@ -3160,4 +3160,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "3.10" -content-hash = "1b30aada5b95507ef134e0728e0fe3871b23407ad25806dbd27a3c88a1806029" +content-hash = "18d11a61394642b3f7c98c170befc8757c6a6687528843d9e0df512d10ba8f36" diff --git a/mini-projects/movie-rec-system/pyproject.toml b/mini-projects/movie-rec-system/pyproject.toml index 21f6b09..925499d 100644 --- a/mini-projects/movie-rec-system/pyproject.toml +++ b/mini-projects/movie-rec-system/pyproject.toml @@ -9,7 +9,7 @@ readme = "README.md" python = "3.10" duckdb = "0.8.1" requests = "^2.31" -pandas = "2.0.1" +pandas = "2.1.0" matplotlib = "3.6.0" python-dotenv="1.0.0" ploomber = "0.22.4" diff --git a/mini-projects/movie-rec-system/tests/test_app.py b/mini-projects/movie-rec-system/tests/test_app.py index e9f7a4c..3dee97f 100644 --- a/mini-projects/movie-rec-system/tests/test_app.py +++ b/mini-projects/movie-rec-system/tests/test_app.py @@ -25,3 +25,27 @@ def test_recommendation_for_nonexistent_movie(): test_data = {"movie": "NonExistentMovie", "num_rec": 5} response = client.post("/recommendations/", json=test_data) assert response.status_code == 404 + + +def test_recommendation_result(): + test_data = {"movie": "Inception", "num_rec": 5} + response = client.post("/recommendations/", json=test_data) + assert response.status_code == 200 + + response_data = response.json() + + assert isinstance(response_data["movie"], str) + + assert isinstance(response_data["recommendations"], list) + assert len(response_data["recommendations"]) == test_data["num_rec"] + + assert isinstance(response_data["metrics"], dict) + metrics = response_data["metrics"] + + assert "popularity" in metrics + assert "vote_avg" in metrics + assert "vote_count" in metrics + + assert isinstance(metrics["popularity"], float) + assert isinstance(metrics["vote_avg"], float) + assert isinstance(metrics["vote_count"], float)