web-dev-qa-db-fra.com

Calculez la distance entre les codes postaux ... et les utilisateurs.

C'est plus une question de défi que quelque chose dont j'ai besoin de toute urgence, alors ne passez pas toute la journée sur ce sujet.

J'ai construit un site de rencontres (disparu depuis longtemps) en 2000 environ, et l'un des défis était de calculer la distance entre les utilisateurs afin que nous puissions présenter vos "correspondances" dans un rayon de X mile. Pour simplement énoncer le problème, étant donné le schéma de base de données suivant (à peu près):

TABLEAU D'UTILISATEURS UserId UserName ZipCode

ZIPCODE TABLE ZipCode Latitude Longitude

Avec USER et ZIPCODE étant joints sur USER.ZipCode = ZIPCODE.ZipCode.

Quelle approche adopteriez-vous pour répondre à la question suivante: Quels autres utilisateurs vivent dans des codes postaux qui sont à moins de X miles du code postal d'un utilisateur donné.

Nous avons utilisé le données du recensement de 20 , qui contient des tableaux pour les codes postaux et leur lattitude et longitude approximatives.

Nous avons également utilisé la formule Haversine pour calculer les distances entre deux points quelconques sur une sphère ... des mathématiques assez simples vraiment.

La question, du moins pour nous, étant les étudiants de 19 ans que nous étions, est vraiment devenue comment calculer et/ou stocker efficacement les distances de tous les membres à tous les autres membres. Une approche (celle que nous avons utilisée) serait d'importer toutes les données et de calculer la distance de chaque code postal à tous les autres codes postaux. Ensuite, vous stockez et indexez les résultats. Quelque chose comme:

SELECT  User.UserId
FROM    ZipCode AS MyZipCode
        INNER JOIN ZipDistance ON MyZipCode.ZipCode = ZipDistance.MyZipCode
        INNER JOIN ZipCode AS TheirZipCode ON ZipDistance.OtherZipCode = TheirZipCode.ZipCode
        INNER JOIN User AS User ON TheirZipCode.ZipCode = User.ZipCode
WHERE   ( MyZipCode.ZipCode = 75044 )
        AND ( ZipDistance.Distance < 50 )

Le problème, bien sûr, est que la table ZipDistance contiendra BEAUCOUP de lignes. Ce n'est pas complètement impraticable, mais c'est vraiment gros. Il nécessite également un pré-travail complet sur l'ensemble des données, ce qui n'est pas non plus ingérable, mais pas nécessairement souhaitable.

Quoi qu'il en soit, je me demandais quelle approche certains d'entre vous, les gourous, pourraient adopter à ce sujet. De plus, je pense que c'est un problème commun que les programmeurs doivent résoudre de temps à autre, surtout si vous considérez des problèmes qui sont simplement similaires sur le plan algorithmique. Je suis intéressé par une solution complète qui inclut au moins des CONSEILS sur toutes les pièces pour que cela se termine très rapidement et efficacement. Merci!

31
bopapa_1979

Ok, pour commencer, vous n'avez pas vraiment besoin d'utiliser la formule Haversine ici. Pour les grandes distances où une formule moins précise produit une erreur plus importante, vos utilisateurs ne se soucient pas si la correspondance est de plus ou moins quelques kilomètres, et pour les distances plus proches, l'erreur est très faible. Il existe des formules plus faciles (à calculer) répertoriées dans l'article Wikipedia Distance géographique .

Étant donné que les codes postaux n'ont rien à voir avec un espacement uniforme, tout processus qui les partitionne uniformément va souffrir fortement dans les zones où ils sont regroupés étroitement (côte est près de DC étant un bon exemple). Si vous voulez une comparaison visuelle, consultez http://benfry.com/zipdecode et comparez le préfixe 89 du code postal avec 07.

Une bien meilleure façon de gérer l'indexation de cet espace consiste à utiliser une structure de données comme un Quadtree ou un R- arbre . Cette structure vous permet d'effectuer des recherches spatiales et à distance sur des données qui ne sont pas régulièrement espacées.

Voici à quoi ressemble un Quadtree:

Quadtree

Pour la rechercher, vous explorez chaque cellule plus grande en utilisant l'index des cellules plus petites qui s'y trouvent. Wikipedia l'explique plus en détail.

Bien sûr, puisque c'est une chose assez courante à faire, quelqu'un d'autre a déjà fait le plus dur pour vous. Comme vous n'avez pas spécifié la base de données que vous utilisez, l'extension PostgreSQL PostGIS servira d'exemple. PostGIS inclut la possibilité de faire des index spatiaux R-tree qui vous permettent d'effectuer des requêtes spatiales efficaces.

Une fois que vous avez importé vos données et construit l'index spatial, la recherche de distance est une requête comme:

SELECT Zip
FROM zipcode
WHERE
geom && expand(transform(PointFromText('POINT(-116.768347 33.911404)', 4269),32661), 16093)
AND
distance(
   transform(PointFromText('POINT(-116.768347 33.911404)', 4269),32661),
   geom) < 16093

Je vous laisse vous-même parcourir le reste du didacticiel.

Voici quelques autres références pour vous aider à démarrer.

33
Paul McMillan

Je voudrais simplement créer une table Zip_code_distances et pré-calculer les distances entre tous les codes postaux 42K aux États-Unis qui sont dans un rayon de 20 à 25 miles les uns des autres.

create table Zip_code_distances
(
from_Zip_code mediumint not null,
to_Zip_code mediumint not null,
distance decimal(6,2) default 0.0,
primary key (from_Zip_code, to_Zip_code),
key (to_Zip_code)
)
engine=innodb;

Le fait d'inclure uniquement des codes postaux dans un rayon de 20 à 25 miles les uns des autres réduit le nombre de lignes que vous devez stocker dans le tableau des distances de son maximum de 1,7 milliard (42K ^ 2) - 42K à environ 4 millions beaucoup plus gérables.

J'ai téléchargé un fichier de données de code postal sur le Web qui contenait les longitudes et latitudes de tous les codes postaux américains officiels au format csv:

"00601","Adjuntas","Adjuntas","Puerto Rico","PR","787","Atlantic", 18.166, -66.7236
"00602","Aguada","Aguada","Puerto Rico","PR","787","Atlantic", 18.383, -67.1866
...
"91210","Glendale","Los Angeles","California","CA","818","Pacific", 34.1419, -118.261
"91214","La Crescenta","Los Angeles","California","CA","818","Pacific", 34.2325, -118.246
"91221","Glendale","Los Angeles","California","CA","818","Pacific", 34.1653, -118.289
...

J'ai écrit un programme C # rapide et sale pour lire le fichier et calculer les distances entre chaque code postal, mais uniquement les codes postaux de sortie qui tombent dans un rayon de 25 miles:

sw = new StreamWriter(path);

foreach (ZipCode fromZip in zips){

    foreach (ZipCode toZip in zips)
    {
        if (toZip.ZipArea == fromZip.ZipArea) continue;

        double dist = ZipCode.GetDistance(fromZip, toZip);

        if (dist > 25) continue;

        string s = string.Format("{0}|{1}|{2}", fromZip.ZipArea, toZip.ZipArea, dist);
        sw.WriteLine(s);
    }
}

Le fichier de sortie résultant se présente comme suit:

from_Zip_code|to_Zip_code|distance
...
00601|00606|16.7042215574185
00601|00611|9.70353520976393
00601|00612|21.0815707704904
00601|00613|21.1780461311929
00601|00614|20.101431539283
...
91210|90001|11.6815708119899
91210|90002|13.3915723402714
91210|90003|12.371251171873
91210|90004|5.26634939906721
91210|90005|6.56649623829871
...

Je voudrais ensuite simplement charger ces données de distance dans ma table Zip_code_distances à l'aide de charger les fichiers de données, puis l'utiliser pour limiter l'espace de recherche de mon application.

Par exemple, si vous avez un utilisateur dont le code postal est 91210 et qu'il souhaite trouver des personnes qui se trouvent dans un rayon de 10 miles de lui, vous pouvez maintenant simplement faire ce qui suit:

select 
 p.*
from
 people p
inner join
(
 select 
  to_Zip_code 
 from 
  Zip_code_distances 
 where 
  from_Zip_code = 91210 and distance <= 10
) search
on p.Zip_code = search.to_Zip_code
where
 p.gender = 'F'....

J'espère que cela t'aides

EDIT: rayon étendu à 100 miles qui a augmenté le nombre de distances par code postal à 32,5 millions de lignes.

vérification rapide des performances pour l'exécution du code postal 91210 0,009 secondes.

select count(*) from Zip_code_distances
count(*)
========
32589820

select 
 to_Zip_code 
from 
 Zip_code_distances 
where 
 from_Zip_code = 91210 and distance <= 10;

0:00:00.009: Query OK
14
Jon Black

Vous pouvez raccourcir le calcul en supposant simplement une boîte au lieu d'un rayon circulaire. Ensuite, lors de la recherche, vous calculez simplement la limite inférieure/supérieure de lat/lon pour un point donné + "rayon", et tant que vous avez un index sur les colonnes lat/lon, vous pouvez retirer tous les enregistrements qui se trouvent dans la boîte assez facilement .

5
babtek

J'utiliserais la latitude et la longitude. Par exemple, si vous avez une latitude de 45 et une longitude de 45 et que l'on vous a demandé de trouver des correspondances à moins de 50 miles, vous pouvez le faire en déplaçant 50/69 ths en latitude et 50/69 ths en latitude (1 deg latitude ~ 69 miles). Sélectionnez les codes postaux avec des latitudes dans cette plage. Les longitudes sont un peu différentes, car elles deviennent plus petites à mesure que vous vous rapprochez des pôles.

Mais à 45 degrés, 1 longitude ~ 49 miles, vous pouvez donc vous déplacer de 50/49ths à gauche en latitude et 50/49ths à droite en latitude, et sélectionner tous les codes postaux à partir de la latitude définie avec cette longitude. Cela vous donne tous les codes postaux dans un carré d'une longueur de cent miles. Si vous voulez être vraiment précis, vous pouvez alors utiliser la formule Haversine que vous avez mentionnée pour éliminer les zips dans les coins de la boîte, pour vous donner une sphère.

1
David Watson

Vous pouvez diviser votre espace en régions de taille à peu près égale - par exemple, rapprocher la Terre d'une boule de bucky ou d'un icosaèdre. Les régions pourraient même se chevaucher un peu, si c'est plus facile (par exemple, les rendre circulaires). Enregistrez dans quelle (s) région (s) chaque code postal se trouve. Ensuite, vous pouvez précalculer la distance maximale possible entre chaque paire de régions, qui a le même O (n ^ 2) problème que de calculer toutes les paires de code postal , mais pour les petits n.

Maintenant, pour un code postal donné, vous pouvez obtenir une liste des régions qui se trouvent définitivement dans votre plage donnée et une liste des régions qui traversent la frontière. Pour les premiers, saisissez simplement tous les codes postaux. Pour ces derniers, explorez chaque région frontalière et calculez en fonction des codes postaux individuels.

C'est certainement plus complexe mathématiquement, et en particulier le nombre de régions devrait être choisi pour un bon équilibre entre la taille de la table et le temps passé à calculer à la volée, mais cela réduit la taille de la table précalculée par une bonne marge.

1
Jander

Toutes les paires de codes postaux possibles ne seront pas utilisées. Je construirais zipdistance en tant que table de "cache". Pour chaque demande, calculez la distance pour cette paire et enregistrez-la dans le cache. Lorsqu'une demande de paire de distance arrive, regardez d'abord dans le cache, puis calculez s'il n'est pas disponible.

Je ne connais pas les subtilités des calculs de distance, donc je vérifierais également si le calcul à la volée est moins cher que la recherche (en tenant également compte de la fréquence à laquelle vous devez calculer).

0
John Smith

J'ai un gros problème, et la réponse de presque tout le monde a été utilisée. Je pensais à cela en termes de l'ancienne solution au lieu de simplement "recommencer". Babtek obtient le feu vert pour avoir déclaré en termes les plus simples.

Je vais sauter le code car je fournirai des références pour dériver les formules nécessaires, et il y a trop de choses à publier proprement ici.

1) Considérons le point A sur une sphère, représentée par la latitude et la longitude. Calculez les bords nord, sud, est et ouest d'une boîte de 2X miles de diamètre avec le point A au centre .

2) Sélectionnez tous les points de la boîte dans le tableau ZipCode. Cela inclut une simple clause WHERE avec deux instructions Between limitant par Lat et Long.

3) Utilisez la formule haversine pour déterminer la distance sphérique entre le point A et chaque point B retourné à l'étape 2.

4) Jeter tous les points B où la distance A -> B> X.

5) Sélectionnez les utilisateurs où ZipCode se trouve dans le reste des points B.

C'est assez rapide pour> 100 miles. Le résultat le plus long était ~ 0,014 seconde pour calculer la correspondance, et trivial pour exécuter l'instruction select.

En outre, comme note latérale, il était nécessaire d'implémenter les mathématiques dans quelques fonctions et de les appeler en SQL. Une fois que j'ai dépassé une certaine distance, le nombre correspondant de ZipCodes était trop important pour être renvoyé à SQL et utilisé comme instruction IN, j'ai donc dû utiliser une table temporaire et joindre les ZipCodes résultants à l'utilisateur dans la colonne ZipCode.

Je soupçonne que l'utilisation d'une table ZipDistance ne fournira pas un gain de performances à long terme. Le nombre de lignes devient vraiment très important. Si vous calculez la distance entre chaque Zip et tous les autres codes postaux (éventuellement), le nombre de lignes résultant de 40 000 codes postaux serait ~ 1,6B. Whoah!

Alternativement, je suis intéressé à utiliser le type de géographie intégré de SQL pour voir si cela facilitera les choses, mais les bons anciens types int/float ont bien servi pour cet exemple.

Alors ... liste finale des ressources en ligne que j'ai utilisées, pour votre référence facile:

1) Différence maximale, latitude et longitude .

2) La formule Haversine .

3) Discussion longue mais complète sur l'ensemble du processus , que j'ai trouvé sur Google dans vos réponses.

0
Eric Burcham

Je sais que ce message est TROP ancien, mais en faisant des recherches pour un client, j'ai trouvé des fonctionnalités utiles de l'API Google Maps et il est si simple à mettre en œuvre, il vous suffit de transmettre à l'URL les codes postaux d'origine et de destination, et il calcule la distance même avec le trafic, vous pouvez l'utiliser avec n'importe quelle langue:

origins = 90210
destinations = 93030
mode = driving

http://maps.googleapis.com/maps/api/distancematrix/json?origins=90210&destinations=93030&mode=driving&language=en-EN&sensor=false%22

en suivant le lien, vous pouvez voir qu'il renvoie un json. N'oubliez pas que vous avez besoin d'une clé API pour l'utiliser sur votre propre hébergement.

source: http://stanhub.com/find-distance-between-two-postcodes-zipcodes-driving-time-in-current-traffic-using-google-maps-api/

0
Facundo Colombier