Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 106 additions & 7 deletions reports/generate_reports_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import pandas as pd
import typer
from calitp_data_analysis.sql import get_engine, query_sql # type: ignore
from siuba import _, arrange, collect # type: ignore
from siuba import _, arrange, collect # type: ignore; , spread # type: ignore
from siuba import filter as filtr # type: ignore
from siuba import mutate, pipe, rename, select, spread # type: ignore
from siuba import mutate, pipe, rename, select # type: ignore; , spread # type: ignore
from siuba.sql import LazyTbl # type: ignore
from tqdm import tqdm

Expand Down Expand Up @@ -250,7 +250,6 @@ def generate_ave_median_vp_age(itp_id: int, date_start, date_end):
]


@cache
def _guideline_check():
return (
LazyTbl(
Expand All @@ -266,33 +265,121 @@ def _guideline_check():
_.reports_status,
_.is_manual,
_.reports_order,
_.percentage,
)
>> collect()
)


def availability_checks(df: pd.DataFrame) -> pd.DataFrame:
"""
Replaces the siuba version for accessibility checks step
using pure pandas operations.
"""
if df.empty:
return df

# === Filter out NAs in reports_order ===
df = df[~df["reports_order"].isna()].copy()

# === Cast reports_order to int ===
df["reports_order"] = df["reports_order"].astype(int)

# === Melt (gather) ===
melted = df.melt(
id_vars=["date_checked", "check", "is_manual", "reports_order"],
value_vars=["reports_status", "percentage"],
var_name="metric",
value_name="value",
)

# Build combined date_stat field
melted["date_stat"] = melted["date_checked"] + "_" + melted["metric"]
melted["date_stat"] = melted["date_stat"].str.replace(
"_reports_status$", "", regex=True
)

# Drop redundant columns
melted = melted.drop(columns=["date_checked", "metric"])

# Pivot (spread)
df_pivot = (
melted.pivot_table(
index=["check", "is_manual", "reports_order"],
columns="date_stat",
values="value",
aggfunc="first",
)
.reset_index()
.fillna("")
)

# Flatten columns
df_pivot.columns = [str(c) for c in df_pivot.columns]

# Reorder columns (swap 4th and 5th)
if df_pivot.shape[1] >= 6:
cols = df_pivot.columns.tolist()
cols = cols[:4] + [cols[5], cols[4]] + cols[6:]
df_pivot = df_pivot[cols]

return df_pivot


def generate_guideline_check(itp_id: int, publish_date, feature):
guideline_check = (
_guideline_check()
>> filtr(_.organization_itp_id == itp_id)
>> filtr(_.publish_date == publish_date)
>> filtr(_.feature == feature)
>> select(
_.date_checked, _.check, _.reports_status, _.is_manual, _.reports_order
_.date_checked,
_.check,
_.reports_status,
_.is_manual,
_.reports_order,
_.percentage,
)
>> mutate(
date_checked=_.date_checked.astype(str),
reports_order=_.reports_order.astype(int),
check=np.where(_.is_manual, _.check + "*", _.check),
)
>> spread(_.date_checked, _.reports_status)
>> pipe(availability_checks)
>> arrange(_.reports_order)
>> pipe(_.fillna(""))
)

return guideline_check


# def generate_guideline_check(itp_id: int, publish_date, feature):
# guideline_check = (
# _guideline_check()
# >> filtr(_.organization_itp_id == itp_id)
# >> filtr(_.publish_date == publish_date)
# >> filtr(_.feature == feature)
# >> filtr(~_.reports_order.isna()) # <-- filter out NAs
# >> select(_.date_checked, _.check, _.reports_status, _.is_manual, _.reports_order, _.percentage
# )
# >> mutate(
# date_checked=_.date_checked.astype(str),
# reports_order = _.reports_order.astype(int),
# check=np.where(_.is_manual, _.check + "*", _.check),
# )
# >> gather("metric", "value", _.reports_status, _.percentage)
# >> mutate(date_stat = _.date_checked + "_" + _.metric)
# >> mutate(date_stat=_.date_stat.str.replace("_reports_status$", "", regex=True))
# >> select(-_.date_checked, -_.metric)
# >> spread(_.date_stat, _.value)
# >> pipe(lambda d: d if (d.empty or d.shape[1] < 6)
# else d[d.columns[:4].tolist() + [d.columns[5], d.columns[4]] + d.columns[6:].tolist()])
# >> arrange(_.reports_order)
# >> pipe(_.fillna(""))
# )

# return guideline_check


@cache
def _validation_codes():
return (
Expand Down Expand Up @@ -455,6 +542,16 @@ def dump_report_data(
with open(out_dir / "4_guideline_checks_rt.json", "w") as f:
json.dump(to_rowspan_table(guideline_checks_rt, "check"), f)

# 4_guideline_checks_accessibility.json
if verbose:
print(f"Generating Accessibility guideline checks for {itp_id}")
guideline_checks_accessibility = generate_guideline_check(
itp_id, publish_date, feature="Accurate Accessibility Data"
)

with open(out_dir / "4_guideline_checks_accessibility.json", "w") as f:
json.dump(to_rowspan_table(guideline_checks_accessibility, "check"), f)

# 5_validation_notices.json
if verbose:
print(f"Generating validation codes for {itp_id}")
Expand Down Expand Up @@ -491,7 +588,9 @@ def generate_data_by_file_path(feed_dir, pbar=None, verbose=False):
if verbose:
print_func = pbar.write if pbar else print
print_func(f"Generating data for file: {feed_dir}")
items = feed_dir.split("/")
# Use Path.parts for cross-platform compatibility (works on Windows and Linux)
# items = feed_dir.split("/")
items = Path(feed_dir).parts
year, month, itp_id = int(items[1]), items[2], items[3]
dates = get_dates_year_month(year, int(month))
date_start, date_end, publish_date = dates[0], dates[1], dates[2]
Expand Down
6 changes: 4 additions & 2 deletions templates/month.html.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@
<li x-bind="reportItem"
{% if loop.first %}:class="{ '!border-t-0': hiddenCount <=0 }"{% endif %}
data-agency-name="{{ entry.agency_name }}"
data-schedule-vendors="{{ entry.schedule_vendors|tojson|forceescape }}"
data-rt-vendors="{{ entry.rt_vendors|tojson|forceescape }}">
{# data-schedule-vendors="{{ entry.schedule_vendors|tojson|forceescape }}" #}
{# data-rt-vendors="{{ entry.rt_vendors|tojson|forceescape }}"> #}
data-schedule-vendors="{{ (entry.schedule_vendors | default([])) | tojson | forceescape }}"
data-rt-vendors="{{ (entry.rt_vendors | default([])) | tojson | forceescape }}">
<a class="list-group__link"
href="{{ SITE_PATH }}/{{ PATH_GTFS_SCHEDULE }}/{{ entry['report_path'] }}">
{# use flex so wrapped items will stay aligned next to icon #}
Expand Down
73 changes: 71 additions & 2 deletions templates/report.html.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{% if entry %}
<i title="present" class="fas fa-lg fa-check-circle text-itp-green"></i>&ZeroWidthSpace;
{% elif showCross %}
<i title="missing" class="fas fa-lg fa-ban text-itp-red/40"></i>&ZeroWidthSpace;
<i title="missing" class="fas fa-lg fa-ban text-itp-red/70"></i>&ZeroWidthSpace;
{% else %}
<i title="missing" class="far fa-lg fa-circle text-itp-slate-light"></i>&ZeroWidthSpace;
{% endif %}
Expand Down Expand Up @@ -37,6 +37,12 @@
</figure>
{% endmacro %}

{% macro status_class(s) -%}
{%- if s == 'PASS' -%}text-itp-green
{%- elif s == 'FAIL' -%}text-itp-red/70
{%- endif -%}
{%- endmacro %}

{%- block title -%}
Monthly GTFS Quality Reports: {{ feed_info.date_month_year }} - {{ feed_info.feed_publisher_name }}
{%- endblock -%}
Expand Down Expand Up @@ -385,7 +391,7 @@
The following symbols are used to denote each check's outcome on the given date:
<ul>
<li> <i class="fas fa-lg fa-check-circle text-itp-green"></i> Pass </li>
<li> <i class="fas fa-lg fa-ban text-itp-red/40"></i> Fail </li>
<li> <i class="fas fa-lg fa-ban text-itp-red/70"></i> Fail </li>
<li> <i class="far fa-lg fa-circle text-itp-slate-light"></i> Not applicable/data unavailable </li>
</ul>
</p>
Expand Down Expand Up @@ -458,6 +464,69 @@
{% endfor %}
</tbody>

{% if guideline_checks_accessibility is defined and guideline_checks_accessibility %}
<!-- column headers -->
<thead class="border-t">
<tr>
<th class="pt-12" colspan="2">
Beyond Compliance Check
</th>
</tr>
<tr>
<th colspan="2">
Accessibility
</th>

<th class="whitespace-nowrap">
{{ guideline_checks_accessibility["columns"][3] }}
</th>
<th class="whitespace-nowrap">
{{ guideline_checks_accessibility["columns"][4] }}
</th>
</tr>
</thead>
<tbody>
{% for row in guideline_checks_accessibility["data"] %}
<tr>
<td colspan="2">
{{ row[0] }}
</td>
<td>
{% set s = row[3] %}
{{ show_check_status(s) }}
{% set v = row[5] %}
{% if (v is number or (v is string and v|trim != '')) and s in ['PASS','FAIL'] %}
<span class="percentage {{ status_class(s) }}">{{ v }}%</span>
{% endif %}
</td>
<td>
{% set s = row[4] %}
{{ show_check_status(s) }}
{% set v = row[6] %}
{% if (v is number or (v is string and v|trim != '')) and s in ['PASS','FAIL'] %}
<span class="percentage {{ status_class(s) }}">{{ v }}%</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
{% else %}
<thead class="border-t">
<tr>
<th class="pt-12" colspan="4">
Beyond Compliance Check
</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="4" class="text-center text-itp-slate-light py-4">
Accessibility data unavailable for this report.
</td>
</tr>
</tbody>
{% endif %}

<!-- validation severity errors -->

{% if validation_codes is not defined %}
Expand Down
5 changes: 5 additions & 0 deletions website/assets/scss/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,8 @@ dialog {
animation: fade-in 250ms;
}
}
.percentage {
margin-left: 4px;
font-size: 0.99em;
font-weight: 900;
}
7 changes: 7 additions & 0 deletions website/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,13 @@ def fetch_report_data(report_dir):
report_data = fetch_report_data(p_report_inputs)
report_data["feeds"] = entry["feeds"]

# Skip if feed_info.json is missing
if "feed_info" not in report_data:
if args.v:
print(f"Skipping {entry['report_path']} — no feed_info.json found")
pbar.update(1)
continue

# track vendors
rt_vendors = report_data["feed_info"]["rt_vendors"]
schedule_vendors = report_data["feed_info"]["schedule_vendors"]
Expand Down