Skip to content

Commit 08b0d60

Browse files
authored
Merge pull request #424 from knaaptime/msafips
msafips
2 parents 14b92cd + 8201743 commit 08b0d60

9 files changed

Lines changed: 144 additions & 128 deletions

File tree

geosnap/analyze/dynamics.py

Lines changed: 70 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@ def transition(
106106
"""
107107
if not w_options:
108108
w_options = {}
109-
assert (
110-
unit_index in gdf.columns
111-
), f"The unit_index ({unit_index}) column is not in the geodataframe"
109+
assert unit_index in gdf.columns, (
110+
f"The unit_index ({unit_index}) column is not in the geodataframe"
111+
)
112112
gdf_temp = gdf.copy().reset_index()
113113
df = gdf_temp[[unit_index, temporal_index, cluster_col]]
114114
df_wide = df.pivot(
@@ -208,9 +208,9 @@ def sequence(
208208
[5., 3., 2., 0., 0.]])
209209
210210
"""
211-
assert (
212-
unit_index in gdf.columns
213-
), f"The unit_index ({unit_index}) column is not in the geodataframe"
211+
assert unit_index in gdf.columns, (
212+
f"The unit_index ({unit_index}) column is not in the geodataframe"
213+
)
214214
gdf_temp = gdf.copy().reset_index()
215215
df = gdf_temp[[unit_index, temporal_index, cluster_col]]
216216
df_wide = (
@@ -287,16 +287,16 @@ def predict_markov_labels(
287287
if not w_options:
288288
w_options = {}
289289

290-
assert (
291-
cluster_col and cluster_col in gdf.columns
292-
), f"The input dataframe has no column named {cluster_col}"
290+
assert cluster_col and cluster_col in gdf.columns, (
291+
f"The input dataframe has no column named {cluster_col}"
292+
)
293293

294-
assert (
295-
base_year
296-
), "Missing `base_year`. You must provide an initial time point with labels to begin simulation"
297-
assert (
298-
base_year in gdf[temporal_index].unique().tolist()
299-
), "A set of observations with `temporal_index`==`base_year` must be included in the gdf"
294+
assert base_year, (
295+
"Missing `base_year`. You must provide an initial time point with labels to begin simulation"
296+
)
297+
assert base_year in gdf[temporal_index].unique().tolist(), (
298+
"A set of observations with `temporal_index`==`base_year` must be included in the gdf"
299+
)
300300

301301
gdf = gdf.copy()
302302
gdf = gdf.dropna(subset=[cluster_col]).reset_index(drop=True)
@@ -310,7 +310,6 @@ def predict_markov_labels(
310310
)
311311

312312
if time_steps == 1:
313-
314313
gdf = gdf[gdf[temporal_index] == base_year].reset_index(drop=True)
315314
w = Ws[w_type].from_dataframe(gdf, **w_options)
316315
predicted = _draw_labels(w, gdf, cluster_col, t, unit_index, verbose)
@@ -319,9 +318,9 @@ def predict_markov_labels(
319318
return predicted
320319

321320
else:
322-
assert (
323-
increment
324-
), "You must set the `increment` argument to simulate multiple time steps"
321+
assert increment, (
322+
"You must set the `increment` argument to simulate multiple time steps"
323+
)
325324
predictions = []
326325
gdf = gdf[gdf[temporal_index] == base_year]
327326
gdf = gdf[[unit_index, cluster_col, temporal_index, gdf.geometry.name]]
@@ -368,8 +367,8 @@ def _draw_labels(w, gdf, cluster_col, markov, unit_index, verbose):
368367
gdf = gdf.copy()
369368
gdf = gdf.dropna(subset=[cluster_col])
370369
lags = lag_categorical(w, gdf[cluster_col].values)
371-
clusters = gdf.reset_index()[cluster_col].astype(str).values
372-
classes = markov.classes.astype(str)
370+
clusters = gdf.reset_index()[cluster_col].values
371+
classes = markov.classes
373372
cluster_idx = dict(zip(classes, list(range(len(classes)))))
374373

375374
labels = {}
@@ -380,9 +379,7 @@ def _draw_labels(w, gdf, cluster_col, markov, unit_index, verbose):
380379
]
381380
# select the class row from the transition matrix using the unit's label
382381
probs = spatial_context[cluster_idx[clusters[i]]]
383-
probs /= (
384-
probs.sum()
385-
) # correct for tolerance, see https://stackoverflow.com/questions/25985120/numpy-1-9-0-valueerror-probabilities-do-not-sum-to-1
382+
probs /= probs.sum() # correct for tolerance, see https://stackoverflow.com/questions/25985120/numpy-1-9-0-valueerror-probabilities-do-not-sum-to-1
386383
probs = np.nan_to_num(probs.flatten())
387384
if sum(probs) == 0:
388385
# in case obs have a modal neighbor never before seen in the model
@@ -391,7 +388,8 @@ def _draw_labels(w, gdf, cluster_col, markov, unit_index, verbose):
391388
if verbose:
392389
warn(
393390
f"Falling back to aspatial transition rule for unit "
394-
f"{gdf[unit_index][i]}"
391+
f"{gdf[unit_index][i]}",
392+
stacklevel=2,
395393
)
396394
probs = markov.p[cluster_idx[clusters[i]]].flatten()
397395

@@ -413,6 +411,7 @@ def draw_sequence_from_gdf(
413411
time_steps=1,
414412
increment=None,
415413
seed=None,
414+
aspatial=False,
416415
):
417416
"""Draw a set of class labels for each unit in a geodataframe using transition
418417
probabilities defined by a giddy.Spatial_Markov model and the spatial lag of each
@@ -463,7 +462,7 @@ def draw_sequence_from_gdf(
463462

464463
dfs = list()
465464

466-
current_df = gdf[gdf[time_column] == start_time]
465+
current_df = gdf[gdf[time_column] == start_time].reset_index()
467466

468467
if seed is None:
469468
seed = int(time())
@@ -478,9 +477,9 @@ def draw_sequence_from_gdf(
478477
# `steps` and `dfs` are off by one so the ith in dfs is the previous time period
479478
current_df = dfs[i].copy()
480479
predicted_labels = _draw_labels_from_gdf(
481-
current_df, w, label_column, smk, generators[i]
480+
current_df, w, label_column, smk, generators[i], aspatial=aspatial
482481
)
483-
predicted_df = gdf[gdf[time_column] == start_time]
482+
predicted_df = gdf[gdf[time_column] == start_time].copy().reset_index()
484483
# overwrite labels with predictions for the new time period
485484
predicted_df[label_column] = predicted_labels
486485
predicted_df[time_column] = step
@@ -491,7 +490,7 @@ def draw_sequence_from_gdf(
491490
return simulated
492491

493492

494-
def _draw_labels_from_gdf(gdf, w, label_column, smk, seed):
493+
def _draw_labels_from_gdf(gdf, w, label_column, smk, seed, aspatial=False):
495494
"""Draw set of new labels given a geodataframe and a spatial Markov transition model
496495
497496
Parameters
@@ -515,16 +514,16 @@ def _draw_labels_from_gdf(gdf, w, label_column, smk, seed):
515514
an array of simulated class labels drawn from the conditional probabilities
516515
provided in the Spatial_Markov object
517516
"""
518-
519-
labels = gdf[label_column].astype(str).values
517+
classes = smk.classes
518+
labels = gdf[label_column].values
520519
lags = lag_categorical(w, labels)
521-
probs = _conditional_probs_from_smk(labels, lags, smk)
522-
assert len(lags) == len(
523-
probs
524-
), "Lag values and probability vectors are different lengths"
525-
simulated_labels = _draw_labels_from_probs(
526-
smk.classes.astype(str), probs=probs, seed=seed
520+
probs = _conditional_probs_from_smk(
521+
labels, lags, smk, fill_null_probs=True, aspatial=aspatial
527522
)
523+
assert len(lags) == len(probs), (
524+
"Lag values and probability vectors are different lengths"
525+
)
526+
simulated_labels = _draw_labels_from_probs(classes, probs=probs, seed=seed)
528527

529528
return simulated_labels
530529

@@ -549,21 +548,19 @@ def _draw_labels_from_probs(classes, probs, seed):
549548
list
550549
list of labels drawn from `classes` with size n_probs
551550
"""
552-
labels = list()
553551
rng = default_rng(seed=seed)
552+
probs = pd.Series(probs)
554553
# for each set of probabilities in the array, draw a label
555554
# this could also be done in parallel. We could also take multiple draws per
556555
# unit by passing a `size` argument to rng.choice
557-
for p in probs:
558-
try:
559-
label = rng.choice(classes, p=p)
560-
except ValueError as e:
561-
raise ValueError from e
562-
labels.append(label)
563-
return np.array(labels)
556+
probs = probs.apply(lambda x: rng.choice(classes, p=x))
557+
558+
return probs.values
564559

565560

566-
def _conditional_probs_from_smk(labels, lags, smk, fill_null_probs=True):
561+
def _conditional_probs_from_smk(
562+
labels, lags, smk, fill_null_probs=True, aspatial=False
563+
):
567564
"""Given a set of existing labels and associated lags, return a vector of
568565
transition probabilities from a giddy.Spatial_Markov model
569566
@@ -583,38 +580,47 @@ def _conditional_probs_from_smk(labels, lags, smk, fill_null_probs=True):
583580
a set of transition probabilities, and each element of the innerr
584581
list(s) is the probability of transitioning into class with index i
585582
"""
586-
classes = smk.classes.astype(str)
583+
# classes = pd.Categorical(smk.classes)
584+
class_idx = dict(zip(smk.classes, range(len(smk.classes))))
587585

588586
# mapping back to the order each class is given in the smk object
589-
class_idx = dict(zip(classes, list(range(len(classes)))))
587+
# class_idx = dict(zip(classes, list(range(len(classes)))))
588+
589+
# print(class_idx)
590590

591591
probs = list()
592592
# this piece could be parallelized as long as it gets concatenated back in order
593-
for i in range(len(labels)):
593+
for i, _ in enumerate(labels):
594594
current_label = labels[i]
595595
current_class_idx = class_idx[current_label]
596+
# print(current_class_idx)
597+
596598
aspatial_p = np.nan_to_num(smk.p[current_class_idx]).flatten()
597599
aspatial_p /= aspatial_p.sum()
598-
600+
# print(i,class_idx[lags[i]])
599601
lag_idx = class_idx[lags[i]]
600602
conditional_matrix = smk.P[lag_idx]
601603
p = conditional_matrix[current_class_idx].flatten()
602604
# correct for tolerance, see https://stackoverflow.com/questions/25985120/numpy-1-9-0-valueerror-probabilities-do-not-sum-to-1
603-
p = np.nan_to_num(p)
604-
if p.sum() == 0:
605-
if fill_null_probs:
606-
warn(
607-
f"No spatial transition rules for {current_label} with conrtext {lags[i]}"
608-
"falling back to aspatial transition rules"
609-
)
610-
probs.append(aspatial_p)
611-
else:
612-
raise ValueError(
613-
f"No spatial transition rules for {current_label} with conrtext {lags[i]}"
614-
)
605+
# p = np.nan_to_num(p)
606+
if aspatial:
607+
probs.append(aspatial_p)
615608
else:
616-
p /= p.sum()
609+
if p.sum() == 0:
610+
if fill_null_probs:
611+
warn(
612+
f"No spatial transition rules for {current_label} with context {lags[i]} "
613+
"falling back to aspatial transition rules",
614+
stacklevel=2,
615+
)
616+
probs.append(aspatial_p)
617+
else:
618+
raise ValueError(
619+
f"No spatial transition rules for {current_label} with context {lags[i]} "
620+
)
621+
else:
622+
# p /= p.sum()
617623

618-
probs.append(np.nan_to_num(p))
624+
probs.append(np.nan_to_num(p))
619625

620626
return probs

geosnap/analyze/geodemo.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ def cluster(
178178
random_state=random_state,
179179
**cluster_kwargs,
180180
)
181-
labels = model.labels_.astype(str)
181+
labels = model.labels_
182182
data = data.reset_index()
183183
clusters = pd.DataFrame(
184184
{
@@ -226,7 +226,7 @@ def cluster(
226226
**cluster_kwargs,
227227
)
228228

229-
labels = pd.Series(model.labels_, name=model_colname).astype(str)
229+
labels = pd.Series(model.labels_, name=model_colname)
230230
clusters = pd.DataFrame(
231231
{
232232
model_colname: labels,
@@ -416,7 +416,7 @@ def regionalize(
416416
**region_kwargs,
417417
)
418418

419-
labels = pd.Series(model.labels_).astype(str)
419+
labels = pd.Series(model.labels_)
420420
clusters = pd.DataFrame(
421421
{
422422
model_colname: labels,

geosnap/io/constructors.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
from warnings import warn
23

34
import geopandas as gpd
@@ -270,7 +271,7 @@ def get_ltdb(
270271
ltdb = datastore.ltdb().reset_index()
271272
if not boundary.crs.equals(4326):
272273
boundary = boundary.copy().to_crs(4326)
273-
tracts = tracts[tracts.representative_point().intersects(boundary.unary_union)]
274+
tracts = tracts[tracts.representative_point().intersects(boundary.union_all())]
274275
gdf = ltdb[ltdb["geoid"].isin(tracts["geoid"])]
275276
gdf = gpd.GeoDataFrame(gdf.merge(tracts, on="geoid", how="left"), crs=4326)
276277
gdf = gdf[gdf["year"].isin(years)]
@@ -336,7 +337,7 @@ def get_ncdb(
336337
ncdb = datastore.ncdb().reset_index()
337338
if not boundary.crs.equals(4326):
338339
boundary = boundary.copy().to_crs(4326)
339-
tracts = tracts[tracts.representative_point().intersects(boundary.unary_union)]
340+
tracts = tracts[tracts.representative_point().intersects(boundary.union_all())]
340341
gdf = ncdb[ncdb["geoid"].isin(tracts["geoid"])]
341342
gdf = gpd.GeoDataFrame(gdf.merge(tracts, on="geoid", how="left"), crs=4326)
342343
gdf = gdf[gdf["year"].isin(years)]
@@ -461,7 +462,7 @@ def get_census(
461462
if isinstance(boundary, gpd.GeoDataFrame):
462463
if not boundary.crs.equals(4326):
463464
boundary = boundary.copy().to_crs(4326)
464-
tracts = tracts[tracts.representative_point().intersects(boundary.unary_union)]
465+
tracts = tracts[tracts.representative_point().intersects(boundary.union_all())]
465466
gdf = tracts.copy()
466467

467468
else:
@@ -574,9 +575,10 @@ def get_lodes(
574575
if boundary.crs != gdf.crs:
575576
warn(
576577
"Unable to determine whether boundary CRS is WGS84 "
577-
"if this produces unexpected results, try reprojecting"
578+
"if this produces unexpected results, try reprojecting",
579+
stacklevel=2,
578580
)
579-
gdf = gdf[gdf.representative_point().intersects(boundary.unary_union)]
581+
gdf = gdf[gdf.representative_point().intersects(boundary.union_all())]
580582

581583
# grab state abbreviations
582584
names = (
@@ -592,19 +594,17 @@ def get_lodes(
592594
for name in names:
593595
merged_year = []
594596
if name == "PR":
595-
raise Exception("does not yet include built-in data for Puerto Rico")
597+
raise ValueError("LODES does not yet include data for Puerto Rico")
596598
try:
597599
df = get_lehd(dataset=dataset, year=year, state=name, version=version)
598600
df = gdf.merge(df, right_index=True, left_on="geoid", how="left")
599601
df["year"] = year
600602
merged_year.append(df)
601603
except ValueError:
602-
warn(f"{name.upper()} {year} not found!")
604+
warn(f"{name.upper()} {year} not found!", stacklevel=2)
603605
pass
604-
try:
606+
with contextlib.suppress(ValueError):
605607
dfs.append(pd.concat(merged_year))
606-
except ValueError:
607-
pass # we've already warned
608608
out = pd.concat(dfs, sort=True)
609609
out = out.groupby(["geoid", "year"]).first().reset_index()
610610
out.crs = 4326
@@ -613,16 +613,14 @@ def get_lodes(
613613

614614
def _msa_to_county(datastore, msa_fips):
615615
if msa_fips is None:
616-
return None
616+
return 0 # dummy integer guaranteed to return no slice from `msa_defs`
617617
msa_defs = datastore.msa_definitions()
618618
pr_metros = set(
619619
msa_defs[msa_defs["CBSA Title"].str.contains("PR")]["CBSA Code"].tolist()
620620
)
621621
if msa_fips in pr_metros:
622-
raise Exception(
623-
"geosnap does not yet include built-in data for Puerto Rico"
624-
)
625-
msa_counties = msa_defs[msa_defs["CBSA Code"] ==int(msa_fips)]["stcofips"].tolist()
622+
raise Exception("geosnap does not yet include built-in data for Puerto Rico")
623+
msa_counties = msa_defs[msa_defs["CBSA Code"] == int(msa_fips)]["stcofips"].tolist()
626624

627625
return msa_counties
628626

0 commit comments

Comments
 (0)