diff --git a/main.py b/main.py index 8c4f66c..21b0d14 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,12 @@ from fastapi import FastAPI, Request from dotenv import load_dotenv -from routers import commonInfo, ageHist, scrapResultLocal, scrapResultMetro +from routers import ( + commonInfo, + ageHist, + scrapResultLocal, + scrapResultMetro, + scrapResultNational, +) from contextlib import asynccontextmanager from typing import Dict from model import MongoDB @@ -35,5 +41,6 @@ async def initMongo(app: FastAPI): app.include_router(scrapResultLocal.router) app.include_router(scrapResultMetro.router) +app.include_router(scrapResultNational.router) app.include_router(commonInfo.router) app.include_router(ageHist.router) diff --git a/model/AgeHist.py b/model/AgeHist.py index a92d140..f02659c 100644 --- a/model/AgeHist.py +++ b/model/AgeHist.py @@ -19,6 +19,10 @@ class AgeHistDataPoint(BaseModel): ageGroup: int +class NationalAgeHistData(BaseModel): + data: list[AgeHistDataPoint] + + class MetroAgeHistData(BaseModel): metroId: int data: list[AgeHistDataPoint] diff --git a/model/BasicResponse.py b/model/BasicResponse.py index e991d0c..06d1217 100644 --- a/model/BasicResponse.py +++ b/model/BasicResponse.py @@ -4,6 +4,7 @@ SUCCESS = 200 REGION_CODE_ERR = 400 COLLECTION_NOT_EXIST_ERR = 600 +NO_DATA_ERROR = 800 class MessageResponse(BaseModel): @@ -15,3 +16,12 @@ class ErrorResponse(BaseModel): error: str code: int message: str + + +NO_DATA_ERROR_RESPONSE: ErrorResponse = ErrorResponse.model_validate( + { + "error": "NoDataError", + "code": NO_DATA_ERROR, + "message": "No data was retrieved with the provided input.", + } +) diff --git a/model/ScrapResultLocal.py b/model/ScrapResultLocal.py index 5a6d16c..9e4ee09 100644 --- a/model/ScrapResultLocal.py +++ b/model/ScrapResultLocal.py @@ -5,7 +5,18 @@ # = Template Data Types = # ============================================== class GenderTemplateDataLocal(BaseModel): + class GenderTemplateDataPoint(BaseModel): + year: int + malePop: int + femalePop: int + + metroId: int + localId: int genderDiversityIndex: float + current: GenderTemplateDataPoint + prev: GenderTemplateDataPoint + meanMalePop: float + meanFemalePop: float class AgeTemplateDataLocal(BaseModel): @@ -53,4 +64,13 @@ class AgeHistogramAreaData(BaseModel): class PartyTemplateDataLocal(BaseModel): + class PartyCountDataPoint(BaseModel): + party: str + count: int + + metroId: int + localId: int partyDiversityIndex: float + prevElected: list[PartyCountDataPoint] + currentElected: list[PartyCountDataPoint] + currentCandidate: list[PartyCountDataPoint] diff --git a/model/ScrapResultMetro.py b/model/ScrapResultMetro.py index 4dea5c9..8d60537 100644 --- a/model/ScrapResultMetro.py +++ b/model/ScrapResultMetro.py @@ -5,7 +5,17 @@ # = Template Data Types = # ============================================== class GenderTemplateDataMetro(BaseModel): + class GenderTemplateDataPoint(BaseModel): + year: int + malePop: int + femalePop: int + + metroId: int genderDiversityIndex: float + current: GenderTemplateDataPoint + prev: GenderTemplateDataPoint + meanMalePop: float + meanFemalePop: float class AgeTemplateDataMetro(BaseModel): @@ -52,4 +62,12 @@ class AgeHistogramAreaData(BaseModel): class PartyTemplateDataMetro(BaseModel): + class PartyCountDataPoint(BaseModel): + party: str + count: int + + metroId: int partyDiversityIndex: float + prevElected: list[PartyCountDataPoint] + currentElected: list[PartyCountDataPoint] + currentCandidate: list[PartyCountDataPoint] diff --git a/model/ScrapResultNational.py b/model/ScrapResultNational.py new file mode 100644 index 0000000..66c847c --- /dev/null +++ b/model/ScrapResultNational.py @@ -0,0 +1,55 @@ +from pydantic import BaseModel + + +# ============================================== +# = Template Data Types = +# ============================================== +class GenderTemplateDataNational(BaseModel): + class GenderTemplateDataPoint(BaseModel): + year: int + malePop: int + femalePop: int + + genderDiversityIndex: float + current: GenderTemplateDataPoint + prev: GenderTemplateDataPoint + + +class AgeTemplateDataNational(BaseModel): + class AgeRankingParagraphData(BaseModel): + ageDiversityIndex: float + + class AgeIndexHistoryParagraphData(BaseModel): + class AgeIndexHistoryIndexData(BaseModel): + year: int + unit: int + candidateCount: int + candidateDiversityIndex: float + candidateDiversityRank: int + electedDiversityIndex: float + electedDiversityRank: int + + mostRecentYear: int + history: list[AgeIndexHistoryIndexData] + + class AgeHistogramParagraphData(BaseModel): + year: int + candidateCount: int + electedCount: int + firstQuintile: int + lastQuintile: int + + rankingParagraph: AgeRankingParagraphData + indexHistoryParagraph: AgeIndexHistoryParagraphData + ageHistogramParagraph: AgeHistogramParagraphData + + +class PartyTemplateDataNational(BaseModel): + class PartyCountDataPoint(BaseModel): + party: str + count: int + + partyDiversityIndex: float + prevElected: list[PartyCountDataPoint] + currentElected: list[PartyCountDataPoint] + currentCandidate: list[PartyCountDataPoint] diff --git a/routers/ageHist.py b/routers/ageHist.py index 4a378ec..14ebec8 100644 --- a/routers/ageHist.py +++ b/routers/ageHist.py @@ -1,46 +1,170 @@ from fastapi import APIRouter from model import BasicResponse, MongoDB -from model.AgeHist import AgeHistDataTypes, AgeHistMethodTypes, MetroAgeHistData +from model.AgeHist import ( + AgeHistDataTypes, + AgeHistMethodTypes, + MetroAgeHistData, + NationalAgeHistData, +) -router = APIRouter() +router = APIRouter(prefix="/age-hist", tags=["age-hist"]) -@router.get("/age-hist/{metroId}") +@router.get("/") +async def getNationalAgeHistData( + ageHistType: AgeHistDataTypes, year: int, method: AgeHistMethodTypes +) -> BasicResponse.ErrorResponse | NationalAgeHistData: + # histogram = await MongoDB.client.stats_db["age_hist"].find_one( + # { + # "councilorType": "national_councilor", + # "is_elected": ageHistType == AgeHistDataTypes.elected, + # "year": year, + # "method": method, + # } + # ) + + # if histogram is None: + # return BasicResponse.ErrorResponse.model_validate( + # { + # "error": "NoDataError", + # "code": BasicResponse.NO_DATA_ERROR, + # "message": "No data retrieved with the provided input.", + # } + # ) + + # return NationalAgeHistData.model_validate({"data": histogram["data"]}) + return NationalAgeHistData.model_validate( + { + "data": [ + { + "minAge": 21, + "maxAge": 22, + "count": 75, + "ageGroup": 0, + }, + { + "minAge": 22, + "maxAge": 23, + "count": 87, + "ageGroup": 1, + }, + { + "minAge": 29, + "maxAge": 30, + "count": 104, + "ageGroup": 2, + }, + { + "minAge": 45, + "maxAge": 46, + "count": 354, + "ageGroup": 2, + }, + { + "minAge": 46, + "maxAge": 47, + "count": 463, + "ageGroup": 3, + }, + { + "minAge": 63, + "maxAge": 64, + "count": 240, + "ageGroup": 4, + }, + ] + } + ) + + +@router.get("/{metroId}") async def getMetroAgeHistData( metroId: int, ageHistType: AgeHistDataTypes, year: int, method: AgeHistMethodTypes ) -> BasicResponse.ErrorResponse | MetroAgeHistData: - if ( - await MongoDB.client.district_db["metro_district"].find_one( - {"metroId": metroId} - ) - is None - ): - return BasicResponse.ErrorResponse.model_validate( - { - "error": "RegionCodeError", - "code": BasicResponse.REGION_CODE_ERR, - "message": f"No metro district with metroId {metroId}.", - } - ) + # if ( + # await MongoDB.client.district_db["metro_district"].find_one( + # {"metroId": metroId} + # ) + # is None + # ): + # return BasicResponse.ErrorResponse.model_validate( + # { + # "error": "RegionCodeError", + # "code": BasicResponse.REGION_CODE_ERR, + # "message": f"No metro district with metroId {metroId}.", + # } + # ) - histogram = await MongoDB.client.stats_db["age_hist"].find_one( + # histogram = await MongoDB.client.stats_db["age_hist"].find_one( + # { + # "level": 1, + # "councilorType": "metro_councilor", + # "is_elected": ageHistType == AgeHistDataTypes.elected, + # "year": year, + # "method": method, + # "metroId": metroId, + # } + # ) + + # if histogram is None: + # return BasicResponse.ErrorResponse.model_validate( + # { + # "error": "NoDataError", + # "code": BasicResponse.NO_DATA_ERROR, + # "message": "No data retrieved with the provided input.", + # } + # ) + + # return MetroAgeHistData.model_validate( + # {"metroId": metroId, "data": histogram["data"]} + # ) + return MetroAgeHistData.model_validate( { - "level": 1, - "councilorType": "metro_councilor", - "is_elected": ageHistType == AgeHistDataTypes.elected, - "year": year, - "method": method, "metroId": metroId, + "data": [ + { + "minAge": 21, + "maxAge": 22, + "count": 75, + "ageGroup": 0, + }, + { + "minAge": 22, + "maxAge": 23, + "count": 87, + "ageGroup": 1, + }, + { + "minAge": 29, + "maxAge": 30, + "count": 104, + "ageGroup": 2, + }, + { + "minAge": 45, + "maxAge": 46, + "count": 354, + "ageGroup": 2, + }, + { + "minAge": 46, + "maxAge": 47, + "count": 463, + "ageGroup": 3, + }, + { + "minAge": 63, + "maxAge": 64, + "count": 240, + "ageGroup": 4, + }, + ], } ) - return MetroAgeHistData.model_validate( - {"metroId": metroId, "data": histogram["data"]} - ) - -@router.get("/age-hist/{metroId}/{localId}") +@router.get("/{metroId}/{localId}") async def getLocalAgeHistData( metroId: int, localId: int, @@ -74,6 +198,15 @@ async def getLocalAgeHistData( } ) + if histogram is None: + return BasicResponse.ErrorResponse.model_validate( + { + "error": "NoDataError", + "code": BasicResponse.NO_DATA_ERROR, + "message": "No data retrieved with the provided input.", + } + ) + return MetroAgeHistData.model_validate( {"metroId": metroId, "localId": localId, "data": histogram["data"]} ) diff --git a/routers/scrapResultLocal.py b/routers/scrapResultLocal.py index b18e5dc..8cd1383 100644 --- a/routers/scrapResultLocal.py +++ b/routers/scrapResultLocal.py @@ -1,6 +1,6 @@ from typing import TypeVar from fastapi import APIRouter -from model.BasicResponse import ErrorResponse, REGION_CODE_ERR +from model.BasicResponse import ErrorResponse, REGION_CODE_ERR, NO_DATA_ERROR_RESPONSE from model.MongoDB import client from model.ScrapResultCommon import ( GenderChartDataPoint, @@ -42,10 +42,97 @@ async def getLocalTemplateData( local_stat = await client.stats_db["diversity_index"].find_one({"localId": localId}) + if local_stat is None: + return NO_DATA_ERROR_RESPONSE + match factor: case FactorType.gender: + years = list( + { + doc["year"] + async for doc in client.stats_db["gender_hist"].find( + { + "councilorType": "local_councilor", + "level": 2, + "is_elected": True, + "localId": localId, + "metroId": metroId, + } + ) + } + ) + years.sort() + assert len(years) >= 2 + + current = await client.stats_db["gender_hist"].find_one( + { + "councilorType": "local_councilor", + "level": 2, + "is_elected": True, + "localId": localId, + "metroId": metroId, + "year": years[-1], + } + ) + + previous = await client.stats_db["gender_hist"].find_one( + { + "councilorType": "local_councilor", + "level": 2, + "is_elected": True, + "localId": localId, + "metroId": metroId, + "year": years[-2], + } + ) + + current_all = ( + await client.stats_db["gender_hist"] + .aggregate( + [ + { + "$match": { + "councilorType": "local_councilor", + "level": 2, + "is_elected": True, + "year": years[-1], + } + }, + { + "$group": { + "_id": None, + "male_tot": {"$sum": "$남"}, + "female_tot": {"$sum": "$여"}, + "district_cnt": {"$sum": 1}, + } + }, + ] + ) + .to_list(500) + ) + assert len(current_all) == 1 + current_all = current_all[0] + return GenderTemplateDataLocal.model_validate( - {"genderDiversityIndex": local_stat["genderDiversityIndex"]} + { + "metroId": metroId, + "localId": localId, + "genderDiversityIndex": local_stat["genderDiversityIndex"], + "current": { + "year": years[-1], + "malePop": current["남"], + "femalePop": current["여"], + }, + "prev": { + "year": years[-2], + "malePop": previous["남"], + "femalePop": previous["여"], + }, + "meanMalePop": current_all["male_tot"] + / current_all["district_cnt"], + "meanFemalePop": current_all["female_tot"] + / current_all["district_cnt"], + } ) case FactorType.age: @@ -71,7 +158,12 @@ async def getLocalTemplateData( # indexHistoryParagraph # ============================ years = list( - {doc["year"] async for doc in client.stats_db["age_hist"].find()} + { + doc["year"] + async for doc in client.stats_db["age_hist"].find( + {"councilorType": "local_councilor"} + ) + } ) years.sort() history_candidate = [ @@ -233,23 +325,111 @@ async def getLocalTemplateData( case FactorType.party: party_diversity_index = local_stat["partyDiversityIndex"] - return PartyTemplateDataLocal.model_validate( - {"partyDiversityIndex": party_diversity_index} + years = list( + { + doc["year"] + async for doc in client.stats_db["party_hist"].find( + { + "councilorType": "local_councilor", + "level": 2, + "is_elected": True, + "localId": localId, + "metroId": metroId, + } + ) + } ) + years.sort() + assert len(years) >= 2 + current_elected = client.stats_db["party_hist"].find( + { + "councilorType": "local_councilor", + "level": 2, + "is_elected": True, + "localId": localId, + "metroId": metroId, + "year": years[-1], + }, + { + "_id": 0, + "councilorType": 0, + "level": 0, + "is_elected": 0, + "localId": 0, + "metroId": 0, + "year": 0, + }, + ) + current_candidate = client.stats_db["party_hist"].find( + { + "councilorType": "local_councilor", + "level": 2, + "is_elected": False, + "localId": localId, + "metroId": metroId, + "year": years[-1], + }, + { + "_id": 0, + "councilorType": 0, + "level": 0, + "is_elected": 0, + "localId": 0, + "metroId": 0, + "year": 0, + }, + ) + previous = client.stats_db["party_hist"].find( + { + "councilorType": "local_councilor", + "level": 2, + "is_elected": True, + "localId": localId, + "metroId": metroId, + "year": years[-2], + }, + { + "_id": 0, + "councilorType": 0, + "level": 0, + "is_elected": 0, + "localId": 0, + "metroId": 0, + "year": 0, + }, + ) -T = TypeVar( - "T", - GenderChartDataPoint, - AgeChartDataPoint, - PartyChartDataPoint, -) + return PartyTemplateDataLocal.model_validate( + { + "metroId": metroId, + "localId": localId, + "partyDiversityIndex": party_diversity_index, + "prevElected": [ + {"party": party, "count": doc[party]} + async for doc in previous + for party in doc + ], + "currentElected": [ + {"party": party, "count": doc[party]} + async for doc in current_elected + for party in doc + ], + "currentCandidate": [ + {"party": party, "count": doc[party]} + async for doc in current_candidate + for party in doc + ], + } + ) @router.get("/chart-data/{metroId}/{localId}") async def getLocalChartData( metroId: int, localId: int, factor: FactorType -) -> ErrorResponse | ChartData[T]: +) -> ErrorResponse | ChartData[GenderChartDataPoint] | ChartData[ + AgeChartDataPoint +] | ChartData[PartyChartDataPoint]: if ( await client.district_db["local_district"].find_one( {"localId": localId, "metroId": metroId} @@ -264,45 +444,98 @@ async def getLocalChartData( } ) - councilors = client.council_db["local_councilor"].find({"localId": localId}) - match factor: case FactorType.gender: - gender_list = [councilor["gender"] async for councilor in councilors] - gender_count = diversity.count(gender_list) + gender_cnt = ( + await client.stats_db["gender_hist"] + .find( + { + "councilorType": "local_councilor", + "level": 2, + "is_elected": True, + "localId": localId, + "metroId": metroId, + } + ) + .sort({"year": -1}) + .limit(1) + .to_list(5) + )[0] + return ChartData[GenderChartDataPoint].model_validate( { "data": [ - {"gender": gender, "count": gender_count[gender]} - for gender in gender_count + {"gender": "남", "count": gender_cnt["남"]}, + {"gender": "여", "count": gender_cnt["여"]}, ] } ) case FactorType.age: - age_list = [councilor["age"] async for councilor in councilors] - age_count = diversity.count(age_list, stair=AGE_STAIR) + age_cnt = ( + await client.stats_db["age_hist"] + .find( + { + "councilorType": "local_councilor", + "level": 2, + "is_elected": True, + "method": "equal", + "localId": localId, + "metroId": metroId, + } + ) + .sort({"year": -1}) + .limit(1) + .to_list(5) + )[0] + age_list = [ + age["minAge"] for age in age_cnt["data"] for _ in range(age["count"]) + ] + age_stair = diversity.count(age_list, stair=AGE_STAIR) return ChartData[AgeChartDataPoint].model_validate( { "data": [ { "minAge": age, "maxAge": age + AGE_STAIR, - "count": age_count[age], + "count": age_stair[age], } - for age in age_count + for age in age_stair ] } ) case FactorType.party: - party_list = [councilor["jdName"] async for councilor in councilors] - party_count = diversity.count(party_list) + party_count = ( + await client.stats_db["party_hist"] + .find( + { + "councilorType": "local_councilor", + "level": 2, + "is_elected": True, + "localId": localId, + "metroId": metroId, + } + ) + .sort({"year": -1}) + .limit(1) + .to_list(5) + )[0] return ChartData[PartyChartDataPoint].model_validate( { "data": [ {"party": party, "count": party_count[party]} for party in party_count + if party + not in [ + "_id", + "councilorType", + "level", + "is_elected", + "localId", + "metroId", + "year", + ] ] } ) diff --git a/routers/scrapResultMetro.py b/routers/scrapResultMetro.py index cef0e72..28cd395 100644 --- a/routers/scrapResultMetro.py +++ b/routers/scrapResultMetro.py @@ -42,8 +42,88 @@ async def getMetroTemplateData( match factor: case FactorType.gender: + years = list( + { + doc["year"] + async for doc in client.stats_db["gender_hist"].find( + { + "councilorType": "metro_councilor", + "level": 1, + "is_elected": True, + "metroId": metroId, + } + ) + } + ) + years.sort() + assert len(years) >= 2 + + current = await client.stats_db["gender_hist"].find_one( + { + "councilorType": "metro_councilor", + "level": 1, + "is_elected": True, + "metroId": metroId, + "year": years[-1], + } + ) + + previous = await client.stats_db["gender_hist"].find_one( + { + "councilorType": "metro_councilor", + "level": 1, + "is_elected": True, + "metroId": metroId, + "year": years[-2], + } + ) + + current_all = ( + await client.stats_db["gender_hist"] + .aggregate( + [ + { + "$match": { + "councilorType": "metro_councilor", + "level": 1, + "is_elected": True, + "year": years[-1], + } + }, + { + "$group": { + "_id": None, + "male_tot": {"$sum": "$남"}, + "female_tot": {"$sum": "$여"}, + "district_cnt": {"$sum": 1}, + } + }, + ] + ) + .to_list(500) + ) + assert len(current_all) == 1 + current_all = current_all[0] + return GenderTemplateDataMetro.model_validate( - {"genderDiversityIndex": metro_stat["genderDiversityIndex"]} + { + "metroId": metroId, + "genderDiversityIndex": metro_stat["genderDiversityIndex"], + "current": { + "year": years[-1], + "malePop": current["남"], + "femalePop": current["여"], + }, + "prev": { + "year": years[-2], + "malePop": previous["남"], + "femalePop": previous["여"], + }, + "meanMalePop": current_all["male_tot"] + / current_all["district_cnt"], + "meanFemalePop": current_all["female_tot"] + / current_all["district_cnt"], + } ) case FactorType.age: @@ -67,7 +147,12 @@ async def getMetroTemplateData( # indexHistoryParagraph # ============================ years = list( - {doc["year"] async for doc in client.stats_db["age_hist"].find()} + { + doc["year"] + async for doc in client.stats_db["age_hist"].find( + {"councilorType": "metro_councilor"} + ) + } ) years.sort() history_candidate = [ @@ -100,65 +185,65 @@ async def getMetroTemplateData( # ============================ # ageHistogramParagraph # ============================ - age_stat_elected = ( - await client.stats_db["age_stat"] - .aggregate( - [ - { - "$match": { - "level": 1, - "councilorType": "metro_councilor", - "is_elected": True, - "metroId": metroId, - } - }, - {"$sort": {"year": -1}}, - {"$limit": 1}, - ] - ) - .to_list(500) - )[0] - most_recent_year = age_stat_elected["year"] - age_stat_candidate = await client.stats_db["age_stat"].find_one( - { - "level": 1, - "councilorType": "metro_councilor", - "is_elected": False, - "metroId": metroId, - "year": most_recent_year, - } - ) + # age_stat_elected = ( + # await client.stats_db["age_hist"] + # .aggregate( + # [ + # { + # "$match": { + # "level": 1, + # "councilorType": "metro_councilor", + # "is_elected": True, + # "metroId": metroId, + # } + # }, + # {"$sort": {"year": -1}}, + # {"$limit": 1}, + # ] + # ) + # .to_list(500) + # )[0] + # most_recent_year = age_stat_elected["year"] + # age_stat_candidate = await client.stats_db["age_hist"].find_one( + # { + # "level": 1, + # "councilorType": "metro_councilor", + # "is_elected": False, + # "metroId": metroId, + # "year": most_recent_year, + # } + # ) - divArea_id = ( - await client.stats_db["diversity_index"].find_one( - {"metroId": {"$exists": True}, "ageDiversityRank": 1} - ) - )["metroId"] - divArea = await client.stats_db["age_stat"].find_one( - { - "level": 1, - "councilorType": "metro_councilor", - "is_elected": True, - "metroId": divArea_id, - "year": most_recent_year, - } - ) + # divArea_id = ( + # await client.stats_db["diversity_index"].find_one( + # {"metroId": {"$exists": True}, "ageDiversityRank": 1} + # ) + # )["metroId"] + # divArea = await client.stats_db["age_hist"].find_one( + # { + # "level": 1, + # "councilorType": "metro_councilor", + # "is_elected": True, + # "metroId": divArea_id, + # "year": most_recent_year, + # } + # ) - uniArea_id = ( - await client.stats_db["diversity_index"].find_one( - # {"metroId": {"$exists": True}, "ageDiversityRank": 17} - {"metroId": {"$exists": True}, "ageDiversityRank": 15} - ) - )["metroId"] - uniArea = await client.stats_db["age_stat"].find_one( - { - "level": 1, - "councilorType": "metro_councilor", - "is_elected": True, - "metroId": uniArea_id, - "year": most_recent_year, - } - ) + # uniArea_id = ( + # await client.stats_db["diversity_index"].find_one( + # # {"metroId": {"$exists": True}, "ageDiversityRank": 17} + # {"metroId": {"$exists": True}, "ageDiversityRank": 15} + # ) + # )["metroId"] + # uniArea = await client.stats_db["age_hist"].find_one( + # { + # "level": 1, + # "councilorType": "metro_councilor", + # "is_elected": True, + # "metroId": uniArea_id, + # "year": most_recent_year, + # } + # ) return AgeTemplateDataMetro.model_validate( { @@ -190,31 +275,48 @@ async def getMetroTemplateData( "candidateDiversityRank": history_candidate[idx][ "diversityRank" ], - "electedDiversityIndex": history_elected[idx][ - "diversityIndex" - ], - "electedDiversityRank": history_elected[idx][ - "diversityRank" - ], + # "electedDiversityIndex": history_elected[idx][ + # "diversityIndex" + # ], + # "electedDiversityRank": history_elected[idx][ + # "diversityRank" + # ], + "electedDiversityIndex": 0.003141592, + "electedDiversityRank": 99999, } for idx, year in enumerate(years) ], }, "ageHistogramParagraph": { - "year": most_recent_year, - "candidateCount": age_stat_candidate["data"][0]["population"], - "electedCount": age_stat_elected["data"][0]["population"], - "firstQuintile": age_stat_elected["data"][0]["firstquintile"], - "lastQuintile": age_stat_elected["data"][0]["lastquintile"], + # "year": most_recent_year, + # "candidateCount": age_stat_candidate["data"][0]["population"], + # "electedCount": age_stat_elected["data"][0]["population"], + # "firstQuintile": age_stat_elected["data"][0]["firstquintile"], + # "lastQuintile": age_stat_elected["data"][0]["lastquintile"], + # "divArea": { + # "metroId": divArea_id, + # "firstQuintile": divArea["data"][0]["firstquintile"], + # "lastQuintile": divArea["data"][0]["lastquintile"], + # }, + # "uniArea": { + # "metroId": uniArea_id, + # "firstQuintile": uniArea["data"][0]["firstquintile"], + # "lastQuintile": uniArea["data"][0]["lastquintile"], + # }, + "year": 2022, + "candidateCount": 99999, + "electedCount": 88888, + "firstQuintile": 74, + "lastQuintile": 21, "divArea": { - "metroId": divArea_id, - "firstQuintile": divArea["data"][0]["firstquintile"], - "lastQuintile": divArea["data"][0]["lastquintile"], + "metroId": 1, + "firstQuintile": 45, + "lastQuintile": 20, }, "uniArea": { - "metroId": uniArea_id, - "firstQuintile": uniArea["data"][0]["firstquintile"], - "lastQuintile": uniArea["data"][0]["lastquintile"], + "metroId": 8, + "firstQuintile": 86, + "lastQuintile": 43, }, }, } @@ -222,8 +324,94 @@ async def getMetroTemplateData( case FactorType.party: party_diversity_index = metro_stat["partyDiversityIndex"] + years = list( + { + doc["year"] + async for doc in client.stats_db["party_hist"].find( + { + "councilorType": "metro_councilor", + "level": 1, + "is_elected": True, + "metroId": metroId, + } + ) + } + ) + years.sort() + assert len(years) >= 2 + + current_elected = client.stats_db["party_hist"].find( + { + "councilorType": "metro_councilor", + "level": 1, + "is_elected": True, + "metroId": metroId, + "year": years[-1], + }, + { + "_id": 0, + "councilorType": 0, + "level": 0, + "is_elected": 0, + "metroId": 0, + "year": 0, + }, + ) + current_candidate = client.stats_db["party_hist"].find( + { + "councilorType": "metro_councilor", + "level": 1, + "is_elected": False, + "metroId": metroId, + "year": years[-1], + }, + { + "_id": 0, + "councilorType": 0, + "level": 0, + "is_elected": 0, + "metroId": 0, + "year": 0, + }, + ) + previous = client.stats_db["party_hist"].find( + { + "councilorType": "metro_councilor", + "level": 1, + "is_elected": True, + "metroId": metroId, + "year": years[-2], + }, + { + "_id": 0, + "councilorType": 0, + "level": 0, + "is_elected": 0, + "metroId": 0, + "year": 0, + }, + ) + return PartyTemplateDataMetro.model_validate( - {"partyDiversityIndex": party_diversity_index} + { + "metroId": metroId, + "partyDiversityIndex": party_diversity_index, + "prevElected": [ + {"party": party, "count": doc[party]} + async for doc in previous + for party in doc + ], + "currentElected": [ + {"party": party, "count": doc[party]} + async for doc in current_elected + for party in doc + ], + "currentCandidate": [ + {"party": party, "count": doc[party]} + async for doc in current_candidate + for party in doc + ], + } ) @@ -236,9 +424,11 @@ async def getMetroTemplateData( @router.get("/chart-data/{metroId}") -async def getLocalChartData( +async def getMetroChartData( metroId: int, factor: FactorType -) -> ErrorResponse | ChartData[T]: +) -> ErrorResponse | ChartData[GenderChartDataPoint] | ChartData[ + AgeChartDataPoint +] | ChartData[PartyChartDataPoint]: if ( await client.district_db["metro_district"].find_one({"metroId": metroId}) is None @@ -251,45 +441,110 @@ async def getLocalChartData( } ) - councilors = client.council_db["metro_councilor"].find({"metroId": metroId}) - match factor: case FactorType.gender: - gender_list = [councilor["gender"] async for councilor in councilors] - gender_count = diversity.count(gender_list) + gender_cnt = ( + await client.stats_db["gender_hist"] + .find( + { + "councilorType": "metro_councilor", + "level": 1, + "is_elected": True, + "metroId": metroId, + } + ) + .sort({"year": -1}) + .limit(1) + .to_list(5) + )[0] + return ChartData[GenderChartDataPoint].model_validate( { "data": [ - {"gender": gender, "count": gender_count[gender]} - for gender in gender_count + {"gender": "남", "count": gender_cnt["남"]}, + {"gender": "여", "count": gender_cnt["여"]}, ] } ) case FactorType.age: - age_list = [councilor["age"] async for councilor in councilors] - age_count = diversity.count(age_list, stair=AGE_STAIR) + # age_cnt = ( + # await client.stats_db["age_hist"] + # .find( + # { + # "councilorType": "metro_councilor", + # "level": 1, + # "is_elected": True, + # "method": "equal", + # "metroId": metroId, + # } + # ) + # .sort({"year": -1}) + # .limit(1) + # .to_list(5) + # )[0] + # age_list = [ + # age["minAge"] for age in age_cnt["data"] for _ in range(age["count"]) + # ] + # age_stair = diversity.count(age_list, stair=AGE_STAIR) + # return ChartData[AgeChartDataPoint].model_validate( + # { + # "data": [ + # { + # "minAge": age, + # "maxAge": age + AGE_STAIR, + # "count": age_stair[age], + # } + # for age in age_stair + # ] + # } + # ) return ChartData[AgeChartDataPoint].model_validate( { "data": [ { - "minAge": age, - "maxAge": age + AGE_STAIR, - "count": age_count[age], - } - for age in age_count + "minAge": 20, + "maxAge": 30, + "count": 888, + }, + { + "minAge": 50, + "maxAge": 60, + "count": 999, + }, ] } ) case FactorType.party: - party_list = [councilor["jdName"] async for councilor in councilors] - party_count = diversity.count(party_list) + party_count = ( + await client.stats_db["party_hist"] + .find( + { + "councilorType": "metro_councilor", + "level": 1, + "is_elected": True, + "metroId": metroId, + } + ) + .sort({"year": -1}) + .limit(1) + .to_list(5) + )[0] return ChartData[PartyChartDataPoint].model_validate( { "data": [ {"party": party, "count": party_count[party]} for party in party_count + if party + not in [ + "_id", + "councilorType", + "level", + "is_elected", + "metroId", + "year", + ] ] } ) diff --git a/routers/scrapResultNational.py b/routers/scrapResultNational.py new file mode 100644 index 0000000..3cee913 --- /dev/null +++ b/routers/scrapResultNational.py @@ -0,0 +1,401 @@ +from typing import TypeVar +from fastapi import APIRouter +from model.BasicResponse import ErrorResponse, NO_DATA_ERROR_RESPONSE +from model.MongoDB import client +from model.ScrapResultCommon import ( + GenderChartDataPoint, + AgeChartDataPoint, + PartyChartDataPoint, + FactorType, + ChartData, +) +from model.ScrapResultNational import ( + GenderTemplateDataNational, + AgeTemplateDataNational, + PartyTemplateDataNational, +) +from utils import diversity + + +router = APIRouter(prefix="/nationalCouncil", tags=["nationalCouncil"]) + +AGE_STAIR = 10 + + +@router.get("/template-data") +async def getNationalTemplateData( + factor: FactorType, +) -> ErrorResponse | GenderTemplateDataNational | AgeTemplateDataNational | PartyTemplateDataNational: + national_stat = await client.stats_db["diversity_index"].find_one( + {"national": True} + ) + if national_stat is None: + return NO_DATA_ERROR_RESPONSE + + match factor: + case FactorType.gender: + years = list( + { + doc["year"] + async for doc in client.stats_db["gender_hist"].find( + { + "councilorType": "national_councilor", + "level": 0, + "is_elected": True, + } + ) + } + ) + years.sort() + assert len(years) >= 2 + + current = await client.stats_db["gender_hist"].find_one( + { + "councilorType": "national_councilor", + "level": 0, + "is_elected": True, + "year": years[-1], + } + ) + + previous = await client.stats_db["gender_hist"].find_one( + { + "councilorType": "national_councilor", + "level": 0, + "is_elected": True, + "year": years[-1], + } + ) + + return GenderTemplateDataNational.model_validate( + { + "genderDiversityIndex": national_stat["genderDiversityIndex"], + "current": { + "year": years[-1], + "malePop": current["남"], + "femalePop": current["여"], + }, + "prev": { + "year": years[-2], + "malePop": previous["남"], + "femalePop": previous["여"], + }, + } + ) + + case FactorType.age: + # ============================ + # rankingParagraph + # ============================ + age_diversity_index = national_stat["ageDiversityIndex"] + + # ============================ + # indexHistoryParagraph + # ============================ + years = list( + { + doc["year"] + async for doc in client.stats_db["age_hist"].find( + {"councilorType": "national_councilor"} + ) + } + ) + years.sort() + history_candidate = [ + await client.stats_db["age_hist"].find_one( + { + "year": year, + "councilorType": "national_councilor", + "is_elected": False, + "method": "equal", + } + ) + for year in years + ] + history_elected = [ + await client.stats_db["age_hist"].find_one( + { + "year": year, + "councilorType": "national_councilor", + "is_elected": True, + "method": "equal", + } + ) + for year in years + ] + + # ============================ + # ageHistogramParagraph + # ============================ + # age_stat_elected = ( + # await client.stats_db["age_stat"] + # .aggregate( + # [ + # { + # "$match": { + # "level": 0, + # "councilorType": "national_councilor", + # "is_elected": True, + # } + # }, + # {"$sort": {"year": -1}}, + # {"$limit": 1}, + # ] + # ) + # .to_list(500) + # )[0] + # most_recent_year = age_stat_elected["year"] + # age_stat_candidate = await client.stats_db["age_stat"].find_one( + # { + # "councilorType": "national_councilor", + # "is_elected": False, + # "year": most_recent_year, + # } + # ) + + return AgeTemplateDataNational.model_validate( + { + "rankingParagraph": { + "ageDiversityIndex": age_diversity_index, + }, + "indexHistoryParagraph": { + # "mostRecentYear": years[-1], + "mostRecentYear": 2022, + "history": [ + { + "year": year, + "unit": (year - 2000) / 4 + 2, + "candidateCount": sum( + group["count"] + for group in history_candidate[idx]["data"] + ), + # "candidateCount": 0, + "candidateDiversityIndex": history_candidate[idx][ + "diversityIndex" + ], + "candidateDiversityRank": history_candidate[idx][ + "diversityRank" + ], + # "candidateDiversityIndex": 0.0, + # "candidateDiversityRank": 0, + "electedDiversityIndex": history_elected[idx][ + "diversityIndex" + ], + "electedDiversityRank": history_elected[idx][ + "diversityRank" + ], + } + for idx, year in enumerate(years) + ], + }, + # "ageHistogramParagraph": { + # "year": most_recent_year, + # "candidateCount": age_stat_candidate["data"][0]["population"], + # "electedCount": age_stat_elected["data"][0]["population"], + # "firstQuintile": age_stat_elected["data"][0]["firstquintile"], + # "lastQuintile": age_stat_elected["data"][0]["lastquintile"], + # }, + "ageHistogramParagraph": { + "year": 2022, + "candidateCount": 99999, + "electedCount": 88888, + "firstQuintile": 98, + "lastQuintile": 18, + }, + } + ) + + case FactorType.party: + party_diversity_index = national_stat["partyDiversityIndex"] + years = list( + { + doc["year"] + async for doc in client.stats_db["party_hist"].find( + { + "councilorType": "national_councilor", + "level": 0, + "is_elected": True, + } + ) + } + ) + years.sort() + assert len(years) >= 2 + + current_elected = client.stats_db["party_hist"].find( + { + "councilorType": "national_councilor", + "level": 0, + "is_elected": True, + "year": years[-1], + }, + { + "_id": 0, + "councilorType": 0, + "level": 0, + "is_elected": 0, + "year": 0, + }, + ) + current_candidate = client.stats_db["party_hist"].find( + { + "councilorType": "national_councilor", + "level": 0, + "is_elected": False, + "year": years[-1], + }, + { + "_id": 0, + "councilorType": 0, + "level": 0, + "is_elected": 0, + "year": 0, + }, + ) + previous = client.stats_db["party_hist"].find( + { + "councilorType": "national_councilor", + "level": 0, + "is_elected": True, + "year": years[-2], + }, + { + "_id": 0, + "councilorType": 0, + "level": 0, + "is_elected": 0, + "year": 0, + }, + ) + + return PartyTemplateDataNational.model_validate( + { + "partyDiversityIndex": party_diversity_index, + "prevElected": [ + {"party": party, "count": doc[party]} + async for doc in previous + for party in doc + ], + "currentElected": [ + {"party": party, "count": doc[party]} + async for doc in current_elected + for party in doc + ], + "currentCandidate": [ + {"party": party, "count": doc[party]} + async for doc in current_candidate + for party in doc + ], + } + ) + + +@router.get("/chart-data") +async def getNationalChartData( + factor: FactorType, +) -> ErrorResponse | ChartData[GenderChartDataPoint] | ChartData[ + AgeChartDataPoint +] | ChartData[PartyChartDataPoint]: + match factor: + case FactorType.gender: + gender_cnt = ( + await client.stats_db["gender_hist"] + .find( + { + "councilorType": "national_councilor", + "level": 0, + "is_elected": True, + } + ) + .sort({"year": -1}) + .limit(1) + .to_list(5) + )[0] + + return ChartData[GenderChartDataPoint].model_validate( + { + "data": [ + {"gender": "남", "count": gender_cnt["남"]}, + {"gender": "여", "count": gender_cnt["여"]}, + ] + } + ) + + case FactorType.age: + # age_cnt = ( + # await client.stats_db["age_hist"] + # .find( + # { + # "councilorType": "national_councilor", + # "level": 0, + # "is_elected": True, + # "method": "equal", + # } + # ) + # .sort({"year": -1}) + # .limit(1) + # .to_list(5) + # )[0] + # age_list = [ + # age["minAge"] for age in age_cnt["data"] for _ in range(age["count"]) + # ] + # age_stair = diversity.count(age_list, stair=AGE_STAIR) + # return ChartData[AgeChartDataPoint].model_validate( + # { + # "data": [ + # { + # "minAge": age, + # "maxAge": age + AGE_STAIR, + # "count": age_stair[age], + # } + # for age in age_stair + # ] + # } + # ) + return ChartData[AgeChartDataPoint].model_validate( + { + "data": [ + { + "minAge": 20, + "maxAge": 30, + "count": 888, + }, + { + "minAge": 50, + "maxAge": 60, + "count": 999, + }, + ] + } + ) + + case FactorType.party: + party_count = ( + await client.stats_db["party_hist"] + .find( + { + "councilorType": "national_councilor", + "level": 0, + "is_elected": True, + } + ) + .sort({"year": -1}) + .limit(1) + .to_list(5) + )[0] + return ChartData[PartyChartDataPoint].model_validate( + { + "data": [ + {"party": party, "count": party_count[party]} + for party in party_count + if party + not in [ + "_id", + "councilorType", + "level", + "is_elected", + "year", + ] + ] + } + )